evolcore 0.0.1 → 0.0.3
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 +64 -0
- package/LICENSE +21 -0
- package/MIGRATION-0.5.0.md +378 -0
- package/README.md +318 -14
- package/ROLE_ACCESS_CONTROL.md +174 -0
- package/assets/.env.template +4 -0
- package/bin/ec-safe-output.js +161 -0
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent.js +163 -0
- package/dist/agents/claude-runner.js +2565 -0
- package/dist/agents/codex-app-server-client.js +448 -0
- package/dist/agents/codex-runner.js +2682 -0
- package/dist/agents/gemini-runner.js +666 -0
- package/dist/agents/runner-types.js +75 -0
- package/dist/aun/aid/agentmd.js +216 -0
- package/dist/aun/aid/client.js +132 -0
- package/dist/aun/aid/control-aid.js +91 -0
- package/dist/aun/aid/identity.js +518 -0
- package/dist/aun/aid/index.js +4 -0
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/group-identity.js +10 -0
- package/dist/aun/msg/group-index.js +6 -0
- package/dist/aun/msg/group.js +1231 -0
- package/dist/aun/msg/history.js +123 -0
- package/dist/aun/msg/index.js +5 -0
- package/dist/aun/msg/p2p.js +393 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +137 -0
- package/dist/aun/outbox.js +168 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +25 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/service-proxy.js +225 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +68 -0
- package/dist/channels/aun.js +4164 -0
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/daemon.js +422 -0
- package/dist/channels/dingtalk.js +1479 -0
- package/dist/channels/feishu.js +1865 -0
- package/dist/channels/qqbot.js +409 -0
- package/dist/channels/wechat.js +817 -0
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +1157 -0
- package/dist/cli/agent-command.js +642 -0
- package/dist/cli/agent.js +1059 -0
- package/dist/cli/aun-commands.js +2003 -0
- package/dist/cli/bench.js +1228 -0
- package/dist/cli/cli-argv.js +66 -0
- package/dist/cli/code-stats.js +329 -0
- package/dist/cli/command-log.js +82 -0
- package/dist/cli/config-selector.js +69 -0
- package/dist/cli/config.js +261 -0
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +62 -0
- package/dist/cli/daemon-commands.js +2750 -0
- package/dist/cli/fs-command.js +1447 -0
- package/dist/cli/handoff-command.js +302 -0
- package/dist/cli/help.js +35 -0
- package/dist/cli/index.js +374 -0
- package/dist/cli/init-channel.js +1372 -0
- package/dist/cli/init.js +590 -0
- package/dist/cli/link-rules.js +240 -0
- package/dist/cli/model.js +591 -0
- package/dist/cli/net-check.js +723 -0
- package/dist/cli/queue-command.js +150 -0
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +344 -0
- package/dist/cli/restart-monitor.js +480 -0
- package/dist/cli/stats.js +609 -0
- package/dist/cli/task-context.js +80 -0
- package/dist/cli/trigger-command.js +545 -0
- package/dist/cli/version.js +93 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +673 -0
- package/dist/config/boot-log.js +266 -0
- package/dist/config/builtin-role-templates.js +42 -0
- package/dist/config/builtin-roles.js +91 -0
- package/dist/config/config-batch-get.js +11 -0
- package/dist/config/config-field-policy.js +261 -0
- package/dist/config/config-manager.js +1120 -0
- package/dist/config/config-operation-service.js +384 -0
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +224 -0
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/gateway-config.js +858 -0
- package/dist/config/lifecycle.js +17 -0
- package/dist/config/mention-mode.js +27 -0
- package/dist/config/merge.js +161 -0
- package/dist/config/owner-policy.js +4 -0
- package/dist/config/peer-role-resolver.js +218 -0
- package/dist/config/resolved-config-op.js +483 -0
- package/dist/config/role-config-v4-startup.js +32 -0
- package/dist/config/role-config-v5-startup.js +27 -0
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-schema.js +105 -0
- package/dist/config/role-service.js +156 -0
- package/dist/config/role-store.js +215 -0
- package/dist/config/roles.js +64 -0
- package/dist/config/schema-registry.js +154 -0
- package/dist/config/snapshot.js +598 -0
- package/dist/config-store.js +501 -0
- package/dist/core/auth/agent-delegation.js +111 -0
- package/dist/core/auth/auth-gateway.js +166 -0
- package/dist/core/auth/authenticated-actor.js +6 -0
- package/dist/core/auth/authorization-audit.js +119 -0
- package/dist/core/auth/operation-authorizer.js +720 -0
- package/dist/core/auth/operation-catalog.js +731 -0
- package/dist/core/baseagent-loader.js +54 -0
- package/dist/core/bootstrap-service.js +175 -0
- package/dist/core/capability/capability-manager.js +316 -0
- package/dist/core/capability/providers/claude-capability-provider.js +176 -0
- package/dist/core/capability/providers/codex-capability-provider.js +148 -0
- package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
- package/dist/core/capability/types.js +27 -0
- package/dist/core/causation/audit.js +103 -0
- package/dist/core/causation/aun-association.js +111 -0
- package/dist/core/causation/context.js +93 -0
- package/dist/core/causation/index.js +4 -0
- package/dist/core/causation/types.js +2 -0
- package/dist/core/channel-loader.js +277 -0
- package/dist/core/command/agent-control.js +616 -0
- package/dist/core/command/cli-intent-parser.js +225 -0
- package/dist/core/command/command-handler.js +1733 -0
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +3452 -0
- package/dist/core/command/menu-protocol.js +247 -0
- package/dist/core/command/role-menu.js +1623 -0
- package/dist/core/command/slash-gate.js +148 -0
- package/dist/core/command/slash-handler.js +3066 -0
- package/dist/core/daemon-file-cache.js +222 -0
- package/dist/core/event-bus.js +32 -0
- package/dist/core/event-catalog.js +810 -0
- package/dist/core/evolagent-registry.js +545 -0
- package/dist/core/evolagent.js +342 -0
- package/dist/core/handoff/dispatcher.js +229 -0
- package/dist/core/handoff/mutex.js +46 -0
- package/dist/core/handoff/runtime.js +324 -0
- package/dist/core/handoff/store.js +537 -0
- package/dist/core/handoff/types.js +12 -0
- package/dist/core/inference/text-inference.js +173 -0
- package/dist/core/interaction-registration.js +10 -0
- package/dist/core/interaction-router.js +278 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/im-renderer.js +659 -0
- package/dist/core/message/items-formatter.js +76 -0
- package/dist/core/message/logical-queue-bridge.js +123 -0
- package/dist/core/message/message-bridge.js +874 -0
- package/dist/core/message/message-cache.js +56 -0
- package/dist/core/message/message-log.js +339 -0
- package/dist/core/message/message-processor.js +4 -0
- package/dist/core/message/message-queue.js +1446 -0
- package/dist/core/message/message-utils.js +76 -0
- package/dist/core/message/peer-mode.js +105 -0
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +33 -0
- package/dist/core/message/response-engine.js +4020 -0
- package/dist/core/message/response-snapshot.js +83 -0
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +139 -0
- package/dist/core/message/stream-idle-monitor.js +124 -0
- package/dist/core/model/config-scope.js +162 -0
- package/dist/core/model/field-scope.js +78 -0
- package/dist/core/model/model-catalog.js +227 -0
- package/dist/core/model/model-diagnostics.js +182 -0
- package/dist/core/model/model-permission.js +90 -0
- package/dist/core/permission/approval-gateway.js +1017 -0
- package/dist/core/permission/ec-command-parser.js +347 -0
- package/dist/core/permission/execution-sandbox.js +16 -0
- package/dist/core/permission/index.js +6 -0
- package/dist/core/permission/mode.js +24 -0
- package/dist/core/permission/sandbox-runtime.js +265 -0
- package/dist/core/permission/tool-policy.js +1019 -0
- package/dist/core/permission/unix-socket-policy.js +99 -0
- package/dist/core/protected-paths.js +332 -0
- package/dist/core/relation/peer-identity.js +222 -0
- package/dist/core/relation/peer-key.js +1 -0
- package/dist/core/role/runtime-policy.js +141 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-fs-store.js +273 -0
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +1643 -0
- package/dist/core/session/session-mapper.js +100 -0
- package/dist/core/session/session-renew.js +314 -0
- package/dist/core/session/session-title.js +128 -0
- package/dist/core/session/session-turn-coordinator.js +205 -0
- package/dist/core/session/session-turns.js +67 -0
- package/dist/core/system-channels.js +29 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/group-rules-sync.js +345 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/kit-renderer.js +359 -0
- package/dist/eck/manifest-engine.js +446 -0
- package/dist/eck/message-renderer.js +199 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +2926 -4
- package/dist/ipc.js +777 -0
- package/dist/paths.js +262 -0
- package/dist/product.js +18 -0
- package/dist/response-system/context-builder.js +71 -0
- package/dist/response-system/coordinator.js +117 -0
- package/dist/response-system/decision-executor.js +86 -0
- package/dist/response-system/engines/v1/index.js +21 -0
- package/dist/response-system/engines/v1/interactive-flow.js +27 -0
- package/dist/response-system/engines/v1/proactive-flow.js +137 -0
- package/dist/response-system/engines/v1/types.js +1 -0
- package/dist/response-system/extensions.js +41 -0
- package/dist/response-system/index.js +6 -0
- package/dist/response-system/modes/index.js +7 -0
- package/dist/response-system/modes/single-session/index.js +72 -0
- package/dist/response-system/queues/fifo-queue.js +44 -0
- package/dist/response-system/queues/index.js +6 -0
- package/dist/response-system/queues/lifo-queue.js +42 -0
- package/dist/response-system/queues/priority-queue.js +63 -0
- package/dist/response-system/registry.js +97 -0
- package/dist/response-system/resolver.js +37 -0
- package/dist/response-system/selector.js +23 -0
- package/dist/response-system/types.js +7 -0
- package/dist/stats/billing.js +163 -0
- package/dist/stats/budget.js +93 -0
- package/dist/stats/db.js +403 -0
- package/dist/stats/eck-vars.js +89 -0
- package/dist/stats/index.js +11 -0
- package/dist/stats/normalizer.js +80 -0
- package/dist/stats/price-resolver.js +138 -0
- package/dist/stats/query.js +763 -0
- package/dist/stats/role-budget.js +168 -0
- package/dist/stats/writer.js +151 -0
- package/dist/trigger/anomaly-store.js +258 -0
- package/dist/trigger/audit.js +152 -0
- package/dist/trigger/event-source.js +119 -0
- package/dist/trigger/feedback.js +685 -0
- package/dist/trigger/history.js +290 -0
- package/dist/trigger/manager.js +294 -0
- package/dist/trigger/parser.js +595 -0
- package/dist/trigger/patch.js +155 -0
- package/dist/trigger/scheduler.js +1602 -0
- package/dist/trigger/script-executor.js +155 -0
- package/dist/trigger/state.js +145 -0
- package/dist/trigger/types.js +1 -0
- package/dist/trigger/validation.js +634 -0
- package/dist/types.js +12 -0
- package/dist/utils/aid-bind.js +313 -0
- package/dist/utils/atomic-write.js +95 -0
- package/dist/utils/avatar-upload.js +123 -0
- package/dist/utils/cross-platform.js +297 -0
- package/dist/utils/ecweb-utils.js +73 -0
- package/dist/utils/error-dict.json +153 -0
- package/dist/utils/error-utils.js +349 -0
- package/dist/utils/instance-registry.js +444 -0
- package/dist/utils/locale.js +21 -0
- package/dist/utils/log-writer.js +270 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/markdown-to-plain-text.js +20 -0
- package/dist/utils/media-cache.js +274 -0
- package/dist/utils/model-prices.jsonl +20 -0
- package/dist/utils/npm-ops.js +210 -0
- package/dist/utils/process-introspect.js +133 -0
- package/dist/utils/process-tree-stats.js +271 -0
- package/dist/utils/project-path.js +74 -0
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stats.js +410 -0
- package/dist/utils/system-memory.js +62 -0
- package/dist/utils/tool-summary.js +284 -0
- package/dist/utils/welcome.js +268 -0
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +66 -0
- package/kits/docs/aun/CHEATSHEET.md +19 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +65 -0
- package/kits/docs/channels/feishu.md +56 -0
- package/kits/docs/context-assembly.md +366 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolcore/INDEX.md +68 -0
- package/kits/docs/evolcore/agent.md +77 -0
- package/kits/docs/evolcore/aid.md +52 -0
- package/kits/docs/evolcore/config.md +149 -0
- package/kits/docs/evolcore/contact.md +57 -0
- package/kits/docs/evolcore/ctl.md +46 -0
- package/kits/docs/evolcore/event.md +216 -0
- package/kits/docs/evolcore/fs-architecture.md +1215 -0
- package/kits/docs/evolcore/fs.md +110 -0
- package/kits/docs/evolcore/group-fs.md +17 -0
- package/kits/docs/evolcore/group-rules.md +226 -0
- package/kits/docs/evolcore/group.md +150 -0
- package/kits/docs/evolcore/model.md +50 -0
- package/kits/docs/evolcore/msg.md +136 -0
- package/kits/docs/evolcore/response.md +75 -0
- package/kits/docs/evolcore/rpc.md +37 -0
- package/kits/docs/evolcore/self-summary.md +29 -0
- package/kits/docs/evolcore/stats.md +83 -0
- package/kits/docs/evolcore/storage.md +50 -0
- package/kits/docs/evolcore/trigger.md +539 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +23 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/docs/prompt-loading-architecture.md +266 -0
- package/kits/docs/venues/aun-group.md +45 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +25 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.auxiliary.json +43 -0
- package/kits/eck_manifest.json +203 -0
- package/kits/eck_message_manifest.json +63 -0
- package/kits/migrations/README-role-config-v4.md +32 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/migrations/migrate-role-config-v4.mjs +623 -0
- package/kits/migrations/migrate-role-config-v5.mjs +346 -0
- package/kits/migrations/rename-config-file.mjs +99 -0
- package/kits/rules/01-overview.md +142 -0
- package/kits/rules/02-navigation.md +76 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +59 -0
- package/kits/rules/05-venue.md +44 -0
- package/kits/rules/06-channel.md +59 -0
- package/kits/schemas/_meta.json +32 -0
- package/kits/schemas/agent-config.schema.1.json +177 -0
- package/kits/schemas/agent-config.schema.2.json +239 -0
- package/kits/schemas/agent-config.schema.3.json +119 -0
- package/kits/schemas/agent-config.schema.4.json +208 -0
- package/kits/schemas/agent-config.schema.5.json +326 -0
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.1.json +36 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/daemon.schema.1.json +90 -0
- package/kits/schemas/defaults.schema.1.json +81 -0
- package/kits/schemas/menu-exec-schema-commands.md +208 -0
- package/kits/schemas/migrations/README.md +28 -0
- package/kits/schemas/relation-config.schema.1.json +158 -0
- package/kits/schemas/relation-config.schema.2.json +73 -0
- package/kits/schemas/relation-config.schema.3.json +50 -0
- package/kits/schemas/relation-config.schema.4.json +47 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +201 -0
- package/kits/schemas/role-registry.schema.1.json +35 -0
- package/kits/schemas/single-session.schema.1.json +31 -0
- package/kits/templates/bootstrap-welcome.md +15 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
- package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/message-fragments/item.md +2 -0
- package/kits/templates/roles/admin.json +9 -0
- package/kits/templates/roles/member.json +41 -0
- package/kits/templates/roles/owner.json +9 -0
- package/kits/templates/roles/visitor.json +40 -0
- package/kits/templates/system-fragments/baseagent.md +14 -0
- package/kits/templates/system-fragments/bootstrap.md +16 -0
- package/kits/templates/system-fragments/channel.md +48 -0
- package/kits/templates/system-fragments/commands.md +28 -0
- package/kits/templates/system-fragments/identity.md +11 -0
- package/kits/templates/system-fragments/relation.md +19 -0
- package/kits/templates/system-fragments/session.md +53 -0
- package/kits/templates/system-fragments/venue.md +31 -0
- package/package.json +50 -15
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,2003 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { agentMdPath, resolveRoot } from '../paths.js';
|
|
3
|
+
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
4
|
+
import { getArgValue, isHelpFlag, wantsHelp } from './help.js';
|
|
5
|
+
// ==================== AID ====================
|
|
6
|
+
function resolveAunPath(args) {
|
|
7
|
+
const idx = args.indexOf('--aun-path');
|
|
8
|
+
if (idx !== -1 && idx + 1 < args.length)
|
|
9
|
+
return args[idx + 1];
|
|
10
|
+
return process.env.AUN_HOME || undefined;
|
|
11
|
+
}
|
|
12
|
+
export async function cmdAid(args) {
|
|
13
|
+
const sub = args[0];
|
|
14
|
+
const formatJson = getArgValue(args, '--format') === 'json';
|
|
15
|
+
const aunPath = resolveAunPath(args);
|
|
16
|
+
if (!sub || isHelpFlag(sub)) {
|
|
17
|
+
console.log(`用法: ec aid <command>
|
|
18
|
+
|
|
19
|
+
Commands:
|
|
20
|
+
list 列出本地所有 AID(实测 sign+verify)
|
|
21
|
+
show <aid> 查看本地 AID 详情(证书、私钥、签名能力)
|
|
22
|
+
new <aid> 创建新 AID 身份
|
|
23
|
+
delete <aid> 删除指定本地 AID(无网络注销)
|
|
24
|
+
delete --orphan 批量清理无私钥的外部 AID 缓存
|
|
25
|
+
delete --no-cert 批量清理无私钥也无公钥证书的孤儿目录
|
|
26
|
+
delete --unrecoverable 批量清理云端公钥已变更、本地不可恢复的 AID
|
|
27
|
+
批量删除默认 dry-run,加 --yes 执行
|
|
28
|
+
lookup <aid> 远程探测 AID(是否存在 + 网关 + agent.md)
|
|
29
|
+
agentmd put <aid> 读本地 agent.md → 签名 → 上传
|
|
30
|
+
agentmd get <aid> 下载 agent.md → 验签 → 本地持久化
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--format json 输出 JSON 格式
|
|
34
|
+
--aun-path <path> 指定 AUN keystore 根(默认 EVOLCORE_HOME,可由 AUN_HOME 设置)
|
|
35
|
+
--help, -h 各子命令均支持,查看详细用法
|
|
36
|
+
|
|
37
|
+
示例:
|
|
38
|
+
ec aid list
|
|
39
|
+
ec aid show toleiliang2.agentid.pub
|
|
40
|
+
ec aid new reviewer.agentid.pub
|
|
41
|
+
ec aid delete --help
|
|
42
|
+
ec aid delete old.agentid.pub
|
|
43
|
+
ec aid delete --orphan
|
|
44
|
+
ec aid delete --unrecoverable --yes
|
|
45
|
+
ec aid lookup someone.agentid.pub
|
|
46
|
+
ec aid agentmd put mybot.agentid.pub
|
|
47
|
+
ec aid agentmd get someone.agentid.pub`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const { aidList, aidListVerified, aidCreate, aidShow, aidDelete, aidLookup, agentmdPut, agentmdGet, buildInitialAgentMd, isValidAid } = await import('../aun/aid/index.js');
|
|
51
|
+
if (sub === 'list') {
|
|
52
|
+
if (wantsHelp(args)) {
|
|
53
|
+
console.log(`用法: ec aid list [筛选选项] [--no-verify] [--format json]
|
|
54
|
+
|
|
55
|
+
列出本地 AID 并跑 sign+verify 自检。
|
|
56
|
+
|
|
57
|
+
筛选选项(可组合,不指定 = 列出 mine + broken + peer-cert):
|
|
58
|
+
--mine 仅本地可用身份(实测可签名+验签通过)
|
|
59
|
+
--broken 仅有私钥但不可用(公钥不匹配 / 证书过期 / sign 失败)
|
|
60
|
+
--peer-cert 仅对端 AID(无私钥,有公钥证书)
|
|
61
|
+
--no-cert 仅无私钥无证书的目录(默认隐藏,需用 aid delete --no-cert 清理)
|
|
62
|
+
|
|
63
|
+
选项:
|
|
64
|
+
--no-verify 跳过 sign+verify 实测,仅静态扫描(更快,mine/broken 仅按静态判定近似)
|
|
65
|
+
--format json JSON 格式输出
|
|
66
|
+
|
|
67
|
+
输出图标:
|
|
68
|
+
🔑 有私钥
|
|
69
|
+
✅ 实测可签名/验签
|
|
70
|
+
❌ 不可签名(公钥不匹配 / sign 失败 / verify 失败等)
|
|
71
|
+
⌛ 证书过期
|
|
72
|
+
📜 有公钥证书
|
|
73
|
+
📄 有 agent.md
|
|
74
|
+
|
|
75
|
+
示例:
|
|
76
|
+
ec aid list 列出 mine + broken + peer-cert
|
|
77
|
+
ec aid list --mine 仅可用身份
|
|
78
|
+
ec aid list --mine --broken 所有有私钥的 AID
|
|
79
|
+
ec aid list --no-cert 仅无私钥无证书的孤儿目录
|
|
80
|
+
ec aid list --no-verify 跳过实测,快速静态扫描`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const wantMine = args.includes('--mine');
|
|
84
|
+
const wantBroken = args.includes('--broken');
|
|
85
|
+
const wantPeerCert = args.includes('--peer-cert');
|
|
86
|
+
const wantNoCert = args.includes('--no-cert');
|
|
87
|
+
const noVerify = args.includes('--no-verify');
|
|
88
|
+
const anyFilter = wantMine || wantBroken || wantPeerCert || wantNoCert;
|
|
89
|
+
// 默认: mine + broken + peer-cert(隐藏 no-cert,需显式 --no-cert 才列)
|
|
90
|
+
const showMine = anyFilter ? wantMine : true;
|
|
91
|
+
const showBroken = anyFilter ? wantBroken : true;
|
|
92
|
+
const showPeerCert = anyFilter ? wantPeerCert : true;
|
|
93
|
+
const showNoCert = anyFilter ? wantNoCert : false;
|
|
94
|
+
const all = noVerify ? aidList(aunPath) : await aidListVerified(aunPath);
|
|
95
|
+
const aids = all.filter(a => (showMine && a.category === 'mine') ||
|
|
96
|
+
(showBroken && a.category === 'broken') ||
|
|
97
|
+
(showPeerCert && a.category === 'peer-cert') ||
|
|
98
|
+
(showNoCert && a.category === 'no-cert'));
|
|
99
|
+
if (formatJson) {
|
|
100
|
+
console.log(JSON.stringify(aids, null, 2));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (aids.length === 0) {
|
|
104
|
+
console.log('无匹配 AID');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.log(`本地 AID${noVerify ? '(静态扫描,未实测)' : ''}(${aunPath ?? resolveRoot()}):`);
|
|
108
|
+
for (const a of aids) {
|
|
109
|
+
const keyIcon = a.hasPrivateKey ? '🔑' : ' ';
|
|
110
|
+
let signIcon = ' ';
|
|
111
|
+
// --no-verify 时 signVerified 始终为 null,用 canSign 作为静态近似
|
|
112
|
+
const effectiveOk = noVerify ? a.canSign : a.signVerified === true;
|
|
113
|
+
const effectiveFail = noVerify ? (a.hasPrivateKey && !a.canSign) : (a.hasPrivateKey && a.signVerified === false);
|
|
114
|
+
if (effectiveOk)
|
|
115
|
+
signIcon = '✅';
|
|
116
|
+
else if (a.hasPrivateKey && a.certExpired)
|
|
117
|
+
signIcon = '⌛';
|
|
118
|
+
else if (effectiveFail)
|
|
119
|
+
signIcon = '❌';
|
|
120
|
+
const certIcon = a.hasCert ? '📜' : ' ';
|
|
121
|
+
const mdIcon = a.hasAgentMd ? '📄' : ' ';
|
|
122
|
+
const tail = !noVerify && a.signVerified === false && a.signError && !(a.keyMatchesCert === false || a.certExpired || !a.hasPrivateKey || !a.hasCert)
|
|
123
|
+
? ` (${a.signError})` : '';
|
|
124
|
+
console.log(` ${keyIcon} ${signIcon} ${certIcon} ${mdIcon} ${a.aid}${tail}`);
|
|
125
|
+
}
|
|
126
|
+
console.log('\n🔑=私钥 ✅=可签名/验签 ❌=不可签名 ⌛=证书过期 📜=公钥证书 📄=agent.md');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (sub === 'show') {
|
|
130
|
+
if (wantsHelp(args)) {
|
|
131
|
+
console.log(`用法: ec aid show <aid> [--format json]
|
|
132
|
+
|
|
133
|
+
查看本地 AID 详情:私钥/证书/agent.md 状态、签名能力实测。`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const aid = args[1];
|
|
137
|
+
if (!aid) {
|
|
138
|
+
console.error('用法: ec aid show <aid>');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
const info = await aidShow(aid, { aunPath });
|
|
142
|
+
if (formatJson) {
|
|
143
|
+
console.log(JSON.stringify(info, null, 2));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
console.log(`AID: ${info.aid}`);
|
|
147
|
+
console.log(` 私钥: ${info.hasPrivateKey ? '有' : '无'}`);
|
|
148
|
+
console.log(` agent.md: ${info.hasAgentMd ? '有' : '无'}`);
|
|
149
|
+
if (info.hasAgentMd) {
|
|
150
|
+
const sigLabel = info.agentMdSignature === 'verified' ? '✓ 已验签'
|
|
151
|
+
: info.agentMdSignature === 'unsigned' ? '⚠ 未签名'
|
|
152
|
+
: info.agentMdSignature === 'invalid' ? `✗ 签名无效${info.agentMdSignatureReason ? ': ' + info.agentMdSignatureReason : ''}`
|
|
153
|
+
: '? 未知';
|
|
154
|
+
console.log(` 签名状态: ${sigLabel}`);
|
|
155
|
+
}
|
|
156
|
+
console.log(` 证书到期: ${info.certExpiresAt ?? '无证书'}${info.certExpired ? ' (已过期!)' : ''}`);
|
|
157
|
+
if (info.certSubject)
|
|
158
|
+
console.log(` 证书主体: ${info.certSubject}`);
|
|
159
|
+
if (info.keyMatchesCert === false)
|
|
160
|
+
console.log(` 密钥/证书: ✗ 公钥不匹配(cert.pem 与 key.json 公钥不一致)`);
|
|
161
|
+
else if (info.keyMatchesCert === true)
|
|
162
|
+
console.log(` 密钥/证书: ✓ 公钥一致`);
|
|
163
|
+
if (info.signVerified === true)
|
|
164
|
+
console.log(` 可签名/验签: ✓ 实测通过`);
|
|
165
|
+
else if (info.signVerified === false)
|
|
166
|
+
console.log(` 可签名/验签: ✗ 失败${info.signError ? `(${info.signError})` : ''}`);
|
|
167
|
+
else
|
|
168
|
+
console.log(` 可签名/验签: ? 未知`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (sub === 'new') {
|
|
172
|
+
if (wantsHelp(args)) {
|
|
173
|
+
console.log(`用法: ec aid new <完整AID> [--force] [--format json]
|
|
174
|
+
|
|
175
|
+
创建新 AID 身份:生成 ECDSA 密钥对、向 Issuer 申请证书、构建并上传初始 agent.md。
|
|
176
|
+
|
|
177
|
+
选项:
|
|
178
|
+
--force 强制重新注册,覆盖已存在的身份(即使签名验证失败)
|
|
179
|
+
--format json 输出 JSON
|
|
180
|
+
--aun-path <path> 指定 AUN keystore 根
|
|
181
|
+
|
|
182
|
+
例: ec aid new reviewer.agentid.pub
|
|
183
|
+
ec aid new reviewer.agentid.pub --force`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const aid = args[1];
|
|
187
|
+
const force = args.includes('--force');
|
|
188
|
+
if (!aid) {
|
|
189
|
+
if (formatJson)
|
|
190
|
+
console.log(JSON.stringify({ ok: false, code: 'MISSING_AID', error: 'missing AID' }));
|
|
191
|
+
else
|
|
192
|
+
console.error('用法: ec aid new <完整AID> [--force]\n例: ec aid new reviewer.agentid.pub');
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
if (!isValidAid(aid)) {
|
|
196
|
+
if (formatJson)
|
|
197
|
+
console.log(JSON.stringify({ ok: false, code: 'INVALID_AID', error: `invalid AID: ${aid}` }));
|
|
198
|
+
else
|
|
199
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const result = await aidCreate(aid, { aunPath, force });
|
|
204
|
+
let agentmdUploaded = result.alreadyExisted;
|
|
205
|
+
let agentmdError;
|
|
206
|
+
if (!result.alreadyExisted) {
|
|
207
|
+
const content = buildInitialAgentMd({ aid });
|
|
208
|
+
try {
|
|
209
|
+
await agentmdPut(content, { aid, aunPath });
|
|
210
|
+
agentmdUploaded = true;
|
|
211
|
+
if (!formatJson)
|
|
212
|
+
console.log('✓ agent.md 已发布');
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
agentmdError = String(e.message || e).slice(0, 100);
|
|
216
|
+
if (!formatJson)
|
|
217
|
+
console.warn(`⚠ agent.md 发布失败(首次连接将自动重试): ${agentmdError}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await result.client.close();
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
try {
|
|
225
|
+
result.store?.close();
|
|
226
|
+
}
|
|
227
|
+
catch { }
|
|
228
|
+
const verb = result.alreadyExisted ? '已存在且有效' : (force ? '已重新创建' : '已创建');
|
|
229
|
+
if (formatJson) {
|
|
230
|
+
console.log(JSON.stringify({
|
|
231
|
+
ok: true,
|
|
232
|
+
aid,
|
|
233
|
+
alreadyExisted: result.alreadyExisted,
|
|
234
|
+
agentmdUploaded,
|
|
235
|
+
...(agentmdError ? { agentmdError } : {}),
|
|
236
|
+
}, null, 2));
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
console.log(`✓ ${aid} ${verb}`);
|
|
240
|
+
console.log(' 如需上线 AUN 通道,运行 ec agent new ' + aid);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
if (e.code === 'AID_INVALID') {
|
|
245
|
+
if (formatJson)
|
|
246
|
+
console.log(JSON.stringify({ ok: false, code: 'AID_INVALID', error: e.message }));
|
|
247
|
+
else
|
|
248
|
+
console.error(`❌ ${e.message}`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
if (e.code === -32052 || e.constructor?.name === 'IdentityConflictError') {
|
|
252
|
+
const error = `AID ${aid} 已在服务端注册,但本地密钥无法匹配;该 AID 可能由其他设备创建,无法在本地恢复`;
|
|
253
|
+
if (formatJson)
|
|
254
|
+
console.log(JSON.stringify({ ok: false, code: 'IDENTITY_CONFLICT', error }));
|
|
255
|
+
else
|
|
256
|
+
console.error(`❌ ${error}。请选择其他名称。`);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
throw e;
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (sub === 'delete') {
|
|
264
|
+
if (wantsHelp(args)) {
|
|
265
|
+
console.log(`用法: ec aid delete <子命令>
|
|
266
|
+
|
|
267
|
+
单个删除:
|
|
268
|
+
ec aid delete <aid> 删除指定 AID 的本地数据(无网络注销)
|
|
269
|
+
|
|
270
|
+
批量删除(默认 dry-run,加 --yes 才真删):
|
|
271
|
+
ec aid delete --orphan 删除所有"无私钥"的本地缓存(外部 AID)
|
|
272
|
+
ec aid delete --no-cert 删除所有"无私钥也无公钥证书"的目录
|
|
273
|
+
条件:!hasPrivateKey && !hasCert
|
|
274
|
+
这些目录最多只剩 agent.md 或 SQLite 残留,
|
|
275
|
+
对验签和加密通信都没用,删除安全。
|
|
276
|
+
ec aid delete --unrecoverable 删除所有不可恢复的 AID
|
|
277
|
+
条件:本地 sign+verify 实测失败
|
|
278
|
+
且 PKI 探测确认云端公钥也不等本地 key.json
|
|
279
|
+
|
|
280
|
+
选项:
|
|
281
|
+
--yes 跳过 dry-run,立即执行
|
|
282
|
+
--skip-pki --unrecoverable 时跳过 PKI 探测,仅依据本地 sign+verify 失败判断(危险,可能误删可恢复 AID)
|
|
283
|
+
--format json 输出 JSON 格式
|
|
284
|
+
--aun-path <path> 指定 AUN keystore 根
|
|
285
|
+
|
|
286
|
+
示例:
|
|
287
|
+
ec aid delete old.agentid.pub
|
|
288
|
+
ec aid delete --orphan 列出会被清理的孤儿
|
|
289
|
+
ec aid delete --orphan --yes 实际清理
|
|
290
|
+
ec aid delete --no-cert 列出无证书孤儿目录
|
|
291
|
+
ec aid delete --no-cert --yes 实际清理
|
|
292
|
+
ec aid delete --unrecoverable 联网探测后列出无救 AID
|
|
293
|
+
ec aid delete --unrecoverable --yes`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const yes = args.includes('--yes');
|
|
297
|
+
const skipPki = args.includes('--skip-pki');
|
|
298
|
+
const orphan = args.includes('--orphan');
|
|
299
|
+
const noCert = args.includes('--no-cert');
|
|
300
|
+
const unrecoverable = args.includes('--unrecoverable');
|
|
301
|
+
const modes = [orphan, noCert, unrecoverable].filter(Boolean).length;
|
|
302
|
+
if (modes > 1) {
|
|
303
|
+
console.error('❌ --orphan / --no-cert / --unrecoverable 互斥,不能同时使用');
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
// 单个 aid 删除:保留原有行为
|
|
307
|
+
if (modes === 0) {
|
|
308
|
+
const aid = args[1];
|
|
309
|
+
if (!aid) {
|
|
310
|
+
console.error('用法: ec aid delete <aid>\n ec aid delete --orphan | --no-cert | --unrecoverable [--yes]\n ec aid delete --help 查看完整用法');
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
const deleted = aidDelete(aid, { aunPath });
|
|
314
|
+
if (deleted) {
|
|
315
|
+
if (formatJson)
|
|
316
|
+
console.log(JSON.stringify({ ok: true, aid, deleted: true }, null, 2));
|
|
317
|
+
else
|
|
318
|
+
console.log(`✓ ${aid} 已删除`);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
if (formatJson)
|
|
322
|
+
console.log(JSON.stringify({ ok: false, aid, deleted: false, error: 'not found' }, null, 2));
|
|
323
|
+
else
|
|
324
|
+
console.error(`❌ 本地不存在: ${aid}`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// 批量模式:先选出候选
|
|
330
|
+
const { probePkiRecoverability } = await import('../aun/aid/index.js');
|
|
331
|
+
const candidates = [];
|
|
332
|
+
if (orphan) {
|
|
333
|
+
const aids = aidList(aunPath);
|
|
334
|
+
for (const a of aids) {
|
|
335
|
+
if (!a.hasPrivateKey)
|
|
336
|
+
candidates.push({ aid: a.aid, reason: 'no private key (external AID cache)' });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else if (noCert) {
|
|
340
|
+
const aids = aidList(aunPath);
|
|
341
|
+
for (const a of aids) {
|
|
342
|
+
if (!a.hasPrivateKey && !a.hasCert) {
|
|
343
|
+
const traits = [a.hasAgentMd ? 'agent.md' : null].filter(Boolean).join(', ');
|
|
344
|
+
candidates.push({ aid: a.aid, reason: `no private key, no cert${traits ? ` (only: ${traits})` : ''}` });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
// unrecoverable: 必须先做 sign+verify 实测
|
|
350
|
+
if (!formatJson)
|
|
351
|
+
console.log('扫描中: 本地签名/验签实测...');
|
|
352
|
+
const aids = await aidListVerified(aunPath);
|
|
353
|
+
const localBroken = aids.filter(a => a.hasPrivateKey && a.signVerified === false);
|
|
354
|
+
if (skipPki) {
|
|
355
|
+
for (const a of localBroken) {
|
|
356
|
+
candidates.push({ aid: a.aid, reason: `sign+verify failed (${a.signError ?? 'unknown'}) [--skip-pki: 未联网验证]` });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
if (!formatJson)
|
|
361
|
+
console.log(`扫描中: 对 ${localBroken.length} 个本地损坏 AID 做 PKI 探测...`);
|
|
362
|
+
for (const a of localBroken) {
|
|
363
|
+
const r = await probePkiRecoverability(a.aid, { aunPath });
|
|
364
|
+
if (r.kind === 'unrecoverable') {
|
|
365
|
+
candidates.push({ aid: a.aid, reason: `local broken; PKI: ${r.reason}`, pki: 'unrecoverable' });
|
|
366
|
+
}
|
|
367
|
+
else if (r.kind === 'no-server-record') {
|
|
368
|
+
candidates.push({ aid: a.aid, reason: `local broken; PKI: ${r.reason}`, pki: 'no-server-record' });
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
// recoverable / no-key / unknown 一律保守不删
|
|
372
|
+
if (!formatJson)
|
|
373
|
+
console.log(` · 跳过 ${a.aid}: PKI=${r.kind}${('reason' in r) ? ' — ' + r.reason : ''}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (formatJson) {
|
|
379
|
+
console.log(JSON.stringify({
|
|
380
|
+
mode: orphan ? 'orphan' : noCert ? 'no-cert' : 'unrecoverable',
|
|
381
|
+
dryRun: !yes,
|
|
382
|
+
skipPki: unrecoverable ? skipPki : undefined,
|
|
383
|
+
candidates,
|
|
384
|
+
}, null, 2));
|
|
385
|
+
if (yes) {
|
|
386
|
+
for (const c of candidates)
|
|
387
|
+
aidDelete(c.aid, { aunPath });
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (candidates.length === 0) {
|
|
392
|
+
console.log(orphan ? '✓ 无孤儿 AID' : noCert ? '✓ 无无证书孤儿目录' : '✓ 无不可恢复 AID');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
console.log(`\n${yes ? '将删除' : '候选删除(dry-run)'}:${candidates.length} 个 AID`);
|
|
396
|
+
for (const c of candidates) {
|
|
397
|
+
console.log(` - ${c.aid}`);
|
|
398
|
+
console.log(` ${c.reason}`);
|
|
399
|
+
}
|
|
400
|
+
if (!yes) {
|
|
401
|
+
console.log('\n(dry-run,未真删除。加 --yes 执行真删。)');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
let ok = 0;
|
|
405
|
+
let fail = 0;
|
|
406
|
+
for (const c of candidates) {
|
|
407
|
+
const deleted = aidDelete(c.aid, { aunPath });
|
|
408
|
+
if (deleted) {
|
|
409
|
+
console.log(` ✓ 删除 ${c.aid}`);
|
|
410
|
+
ok++;
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
console.log(` ✗ 失败 ${c.aid}(已不存在?)`);
|
|
414
|
+
fail++;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
console.log(`\n完成:成功 ${ok},失败 ${fail}`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (sub === 'lookup') {
|
|
421
|
+
if (wantsHelp(args)) {
|
|
422
|
+
console.log(`用法: ec aid lookup <aid> [--format json]
|
|
423
|
+
|
|
424
|
+
远程探测 AID:是否注册、所在网关、是否有 agent.md(不验签,仅获取)。`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const aid = args[1];
|
|
428
|
+
if (!aid) {
|
|
429
|
+
console.error('用法: ec aid lookup <aid>');
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
if (!isValidAid(aid)) {
|
|
433
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
const result = await aidLookup(aid);
|
|
437
|
+
if (formatJson) {
|
|
438
|
+
console.log(JSON.stringify(result, null, 2));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (result.exists) {
|
|
442
|
+
console.log(`✓ ${aid} 已注册`);
|
|
443
|
+
if (result.gateway)
|
|
444
|
+
console.log(` 网关: ${result.gateway}`);
|
|
445
|
+
if (result.content) {
|
|
446
|
+
const hasSig = result.content.includes('AUN-SIGNATURE');
|
|
447
|
+
console.log(` 签名: ${hasSig ? '有(未验证,如需验证请用 ec aid agentmd get ' + aid + ')' : '无'}`);
|
|
448
|
+
console.log('');
|
|
449
|
+
console.log(result.content);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
console.log(`✗ ${aid} 未注册`);
|
|
454
|
+
if (result.gateway)
|
|
455
|
+
console.log(` 网关: ${result.gateway}`);
|
|
456
|
+
if (result.error)
|
|
457
|
+
console.log(` 原因: ${result.error}`);
|
|
458
|
+
}
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (sub === 'agentmd') {
|
|
462
|
+
const verb = args[1];
|
|
463
|
+
const aid = args[2];
|
|
464
|
+
if (!verb || isHelpFlag(verb) || wantsHelp(args)) {
|
|
465
|
+
console.log(`用法: ec aid agentmd <put|get> <aid> [--format json]
|
|
466
|
+
|
|
467
|
+
put <aid> 读本地 agent.md → 用本地私钥签名 → 上传到 PKI
|
|
468
|
+
get <aid> 从 PKI 下载 agent.md → 验签 → 持久化到本地`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (verb === 'put') {
|
|
472
|
+
if (!aid) {
|
|
473
|
+
console.error('用法: ec aid agentmd put <aid>');
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
if (!isValidAid(aid)) {
|
|
477
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
const localPath = agentMdPath(aid);
|
|
481
|
+
if (!fs.existsSync(localPath)) {
|
|
482
|
+
console.error(`❌ 本地无 agent.md: ${aid}`);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
const content = fs.readFileSync(localPath, 'utf-8');
|
|
486
|
+
await agentmdPut(content, { aid, aunPath });
|
|
487
|
+
if (formatJson) {
|
|
488
|
+
console.log(JSON.stringify({ ok: true, aid, path: localPath }, null, 2));
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
console.log('✓ agent.md 已发布');
|
|
492
|
+
}
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (verb === 'get') {
|
|
496
|
+
if (!aid) {
|
|
497
|
+
console.error('用法: ec aid agentmd get <aid>');
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
if (!isValidAid(aid)) {
|
|
501
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const result = await agentmdGet(aid, { withVerification: true, aunPath });
|
|
506
|
+
if (!result.content || !result.content.trim()) {
|
|
507
|
+
console.log(`ℹ️ ${aid} 尚未设置 agent.md`);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (formatJson) {
|
|
511
|
+
console.log(JSON.stringify(result, null, 2));
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
console.log(result.content);
|
|
515
|
+
const v = result.verification;
|
|
516
|
+
if (v.status === 'verified') {
|
|
517
|
+
console.error(`✓ 签名验证通过`);
|
|
518
|
+
}
|
|
519
|
+
else if (v.status === 'invalid') {
|
|
520
|
+
console.error(`⚠ 签名验证失败: ${v.reason ?? '未知原因'}`);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
console.error(`ℹ️ 未签名`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
catch (e) {
|
|
528
|
+
const msg = String(e.message || e);
|
|
529
|
+
if (msg.includes('not found') || msg.includes('404')) {
|
|
530
|
+
console.log(`ℹ️ ${aid} 尚未设置 agent.md`);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
console.error(`❌ 获取失败: ${msg.slice(0, 100)}`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
console.error(`未知子命令: aid agentmd ${verb ?? ''}\n用法: ec aid agentmd [put|get] <aid>`);
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
console.error(`未知子命令: ${sub}\n用法: ec aid [list|show|new|delete|lookup|agentmd] <aid>`);
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
// ==================== RPC ====================
|
|
546
|
+
export async function cmdRpc(args) {
|
|
547
|
+
if (args.length === 0 || wantsHelp(args)) {
|
|
548
|
+
console.log(`用法: ec rpc --as <aid> --params <params>
|
|
549
|
+
|
|
550
|
+
通用 AUN RPC 调用。
|
|
551
|
+
|
|
552
|
+
--params 自动判断输入形式:
|
|
553
|
+
单行 JSON (以 { 开头) → 单次调用
|
|
554
|
+
多行 JSONL → 逐行执行,失败即停
|
|
555
|
+
文件路径 (文件存在) → 读取文件内容作为 JSONL
|
|
556
|
+
|
|
557
|
+
每行 JSON 格式: {"method":"<namespace.method>","params":{...}}
|
|
558
|
+
|
|
559
|
+
Options:
|
|
560
|
+
--app <name> 指定应用 slot(独立消费通道)。仅对 message.pull / group.pull
|
|
561
|
+
等消费类方法有意义——隔离 seq 游标与消息过滤;默认与 daemon 共享通道。
|
|
562
|
+
--aun-path <path> 指定 AUN keystore 根
|
|
563
|
+
|
|
564
|
+
示例:
|
|
565
|
+
ec rpc --as alice.agentid.pub --params '{"method":"message.send","params":{"to":"bob.agentid.pub","payload":{"type":"text","text":"hello"}}}'
|
|
566
|
+
ec rpc --as alice.agentid.pub --params calls.jsonl`);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const asIdx = args.indexOf('--as');
|
|
570
|
+
const paramsIdx = args.indexOf('--params');
|
|
571
|
+
const aunPath = resolveAunPath(args);
|
|
572
|
+
const appSlot = getArgValue(args, '--app');
|
|
573
|
+
if (asIdx === -1 || asIdx + 1 >= args.length) {
|
|
574
|
+
console.error('❌ 缺少 --as <aid>');
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
if (paramsIdx === -1 || paramsIdx + 1 >= args.length) {
|
|
578
|
+
console.error('❌ 缺少 --params <params>');
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
const aid = args[asIdx + 1];
|
|
582
|
+
const paramsRaw = args[paramsIdx + 1];
|
|
583
|
+
const { isValidAid } = await import('../aun/aid/index.js');
|
|
584
|
+
if (!isValidAid(aid)) {
|
|
585
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
// Determine input: file, single JSON, or multi-line JSONL
|
|
589
|
+
let lines;
|
|
590
|
+
if (fs.existsSync(paramsRaw)) {
|
|
591
|
+
lines = fs.readFileSync(paramsRaw, 'utf-8').split('\n').filter(l => l.trim());
|
|
592
|
+
}
|
|
593
|
+
else if (paramsRaw.includes('\n')) {
|
|
594
|
+
lines = paramsRaw.split('\n').filter(l => l.trim());
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
lines = [paramsRaw];
|
|
598
|
+
}
|
|
599
|
+
// Parse calls
|
|
600
|
+
const calls = [];
|
|
601
|
+
for (let i = 0; i < lines.length; i++) {
|
|
602
|
+
try {
|
|
603
|
+
const parsed = JSON.parse(lines[i]);
|
|
604
|
+
if (!parsed.method) {
|
|
605
|
+
console.error(`❌ 第 ${i + 1} 行缺少 "method" 字段`);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
calls.push({ method: parsed.method, params: parsed.params ?? {} });
|
|
609
|
+
}
|
|
610
|
+
catch (e) {
|
|
611
|
+
console.error(`❌ 第 ${i + 1} 行 JSON 解析失败: ${e.message}`);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const { rpcCall, rpcBatch } = await import('../aun/rpc/index.js');
|
|
616
|
+
if (calls.length === 1) {
|
|
617
|
+
const result = await rpcCall(aid, calls[0].method, calls[0].params, { aunPath, slotId: appSlot });
|
|
618
|
+
console.log(JSON.stringify(result));
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
const results = await rpcBatch(aid, calls, { aunPath, slotId: appSlot });
|
|
622
|
+
for (const r of results) {
|
|
623
|
+
console.log(JSON.stringify(r));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// ==================== Storage ====================
|
|
628
|
+
export async function cmdStorage(args) {
|
|
629
|
+
const sub = args[0];
|
|
630
|
+
const aunPath = resolveAunPath(args);
|
|
631
|
+
const formatJson = getArgValue(args, '--format') === 'json';
|
|
632
|
+
if (!sub || wantsHelp(args)) {
|
|
633
|
+
console.log(`用法: ec storage <command> <aid> [options]
|
|
634
|
+
|
|
635
|
+
Commands:
|
|
636
|
+
upload <aid> <local-file> <remote-path> [--public] 上传文件(默认私有)
|
|
637
|
+
download <aid> <url> [local-path] 下载文件
|
|
638
|
+
ls <aid> [prefix] 列文件
|
|
639
|
+
rm <aid> <remote-path> 删文件
|
|
640
|
+
quota <aid> 查配额
|
|
641
|
+
|
|
642
|
+
<url> 格式: [https://]<owner-aid>/<path>
|
|
643
|
+
|
|
644
|
+
Options:
|
|
645
|
+
--public 上传后公开可读(仅 upload)
|
|
646
|
+
--format json 输出 JSON
|
|
647
|
+
--aun-path <path> 指定 AUN keystore 根
|
|
648
|
+
|
|
649
|
+
示例:
|
|
650
|
+
ec storage upload myaid.agentid.pub ./doc.txt notes/doc.txt
|
|
651
|
+
ec storage upload myaid.agentid.pub ./pic.png images/pic.png --public
|
|
652
|
+
ec storage download myaid.agentid.pub myaid.agentid.pub/notes/doc.txt ./doc.txt
|
|
653
|
+
ec storage download myaid.agentid.pub bob.agentid.pub/public/file.pdf ./file.pdf
|
|
654
|
+
ec storage ls myaid.agentid.pub notes/
|
|
655
|
+
ec storage rm myaid.agentid.pub notes/doc.txt
|
|
656
|
+
ec storage quota myaid.agentid.pub`);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const aid = args[1];
|
|
660
|
+
if (!aid) {
|
|
661
|
+
console.error('❌ 缺少 <aid> 参数');
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
const { isValidAid } = await import('../aun/aid/index.js');
|
|
665
|
+
if (!isValidAid(aid)) {
|
|
666
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
const { storageUpload, storageDownload, storageLs, storageRm, storageQuota } = await import('../aun/storage/index.js');
|
|
670
|
+
if (sub === 'upload') {
|
|
671
|
+
const localFile = args[2];
|
|
672
|
+
const remotePath = args[3];
|
|
673
|
+
const isPublic = args.includes('--public');
|
|
674
|
+
if (!localFile || !remotePath) {
|
|
675
|
+
console.error('用法: ec storage upload <aid> <local-file> <remote-path> [--public]');
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
if (!fs.existsSync(localFile)) {
|
|
679
|
+
console.error(`❌ 文件不存在: ${localFile}`);
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
const result = await storageUpload(aid, localFile, remotePath, { isPublic, aunPath });
|
|
683
|
+
if (!result.ok) {
|
|
684
|
+
if (formatJson) {
|
|
685
|
+
console.log(JSON.stringify({ ok: false, error: result.error }));
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
console.error(`❌ 上传失败: ${result.error}`);
|
|
689
|
+
}
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
if (formatJson) {
|
|
693
|
+
console.log(JSON.stringify({ ok: true, objectKey: remotePath, isPublic, ref: `${aid}/${remotePath}`, publicUrl: result.publicUrl ?? null }));
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
console.log(`✓ 已上传: ${remotePath}${isPublic ? ' (公开)' : ''}`);
|
|
697
|
+
if (result.publicUrl) {
|
|
698
|
+
console.log(` 🔗 访问: ${result.publicUrl}`);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
console.log(` 引用: ${aid}/${remotePath}`);
|
|
702
|
+
}
|
|
703
|
+
console.log(` 下载: ec storage download ${aid} ${aid}/${remotePath}`);
|
|
704
|
+
}
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (sub === 'download') {
|
|
708
|
+
const url = args[2];
|
|
709
|
+
const localPath = args[3];
|
|
710
|
+
if (!url) {
|
|
711
|
+
console.error('用法: ec storage download <aid> <url> [local-path]');
|
|
712
|
+
process.exit(1);
|
|
713
|
+
}
|
|
714
|
+
const result = await storageDownload(aid, url, localPath, { aunPath });
|
|
715
|
+
if (!result.ok) {
|
|
716
|
+
if (formatJson) {
|
|
717
|
+
console.log(JSON.stringify({ ok: false, error: result.error }));
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
console.error(`❌ 下载失败: ${result.error}`);
|
|
721
|
+
}
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
if (formatJson) {
|
|
725
|
+
console.log(JSON.stringify({ ok: true, localPath: result.localPath, size: result.size }));
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
console.log(`✓ 已下载: ${result.localPath} (${result.size} bytes)`);
|
|
729
|
+
}
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (sub === 'ls') {
|
|
733
|
+
const prefix = args[2] || '';
|
|
734
|
+
const result = await storageLs(aid, prefix, { aunPath });
|
|
735
|
+
if (!result.ok) {
|
|
736
|
+
console.error(`❌ 列文件失败: ${JSON.stringify(result.error)}`);
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
const objects = result.result?.objects || result.result || [];
|
|
740
|
+
if (Array.isArray(objects) && objects.length === 0) {
|
|
741
|
+
console.log('(空)');
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
console.log(JSON.stringify(objects, null, 2));
|
|
745
|
+
}
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (sub === 'rm') {
|
|
749
|
+
const remotePath = args[2];
|
|
750
|
+
if (!remotePath) {
|
|
751
|
+
console.error('用法: ec storage rm <aid> <remote-path>');
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
754
|
+
const result = await storageRm(aid, remotePath, { aunPath });
|
|
755
|
+
if (!result.ok) {
|
|
756
|
+
if (formatJson) {
|
|
757
|
+
console.log(JSON.stringify({ ok: false, error: result.error }));
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
console.error(`❌ 删除失败: ${JSON.stringify(result.error)}`);
|
|
761
|
+
}
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
if (formatJson) {
|
|
765
|
+
console.log(JSON.stringify({ ok: true, objectKey: remotePath }));
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
console.log(`✓ 已删除: ${remotePath}`);
|
|
769
|
+
}
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (sub === 'quota') {
|
|
773
|
+
const result = await storageQuota(aid, { aunPath });
|
|
774
|
+
if (!result.ok) {
|
|
775
|
+
console.error(`❌ 查询配额失败: ${JSON.stringify(result.error)}`);
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
console.error(`未知子命令: ${sub}\n用法: ec storage [upload|download|ls|rm|quota]`);
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
// ==================== Msg ====================
|
|
785
|
+
export async function cmdMsg(args) {
|
|
786
|
+
const sub = args[0];
|
|
787
|
+
const aunPath = resolveAunPath(args);
|
|
788
|
+
const formatJson = getArgValue(args, '--format') === 'json';
|
|
789
|
+
const appSlot = getArgValue(args, '--app');
|
|
790
|
+
if (!sub || isHelpFlag(sub) || (wantsHelp(args) && sub !== 'history')) {
|
|
791
|
+
console.log(`用法: ec msg <command> <from-aid> [args...] [options]
|
|
792
|
+
|
|
793
|
+
Commands:
|
|
794
|
+
send <from> <to> <text> 发送文本
|
|
795
|
+
send <from> <to> --text-from-file <path> 从文件读取文本内容
|
|
796
|
+
send <from> <to> --file <path> [--as <type>] 发送文件(image|video|voice|file)
|
|
797
|
+
send <from> <to> --link <url> [--title T] 发送链接卡片
|
|
798
|
+
send <from> <to> --payload <json> 发送自定义 payload
|
|
799
|
+
pull <from> [--after-seq N] [--limit N] 拉取收件箱
|
|
800
|
+
ack <from> <seq> [--app <name>] 确认已读
|
|
801
|
+
recall <from> <message-id> [<message-id>...] 撤回消息
|
|
802
|
+
online <from> <target-aid> [<target-aid>...] 查询在线状态
|
|
803
|
+
history <from> <target-aid> [筛选选项] 查询本地消息历史
|
|
804
|
+
|
|
805
|
+
Options:
|
|
806
|
+
--text-from-file <path> 从文件读取文本(UTF-8),用于超长消息或避免 Shell 转义
|
|
807
|
+
--app <name> 指定应用 slot(独立消费通道,不影响 daemon)
|
|
808
|
+
--aun-path <path> 指定 AUN keystore 根
|
|
809
|
+
--format json 输出 JSON 格式
|
|
810
|
+
--encrypt 启用端到端加密(密文发送)
|
|
811
|
+
--no-encrypt 强制明文发送(优先于 --encrypt)
|
|
812
|
+
--thread <id> 指定话题 ID(用于多话题路由)
|
|
813
|
+
--session <latest|all|id> 按 EvolCore 逻辑会话过滤历史(默认 latest)
|
|
814
|
+
--before <time> 只返回该时间之前的历史
|
|
815
|
+
--after <time> 只返回该时间之后的历史
|
|
816
|
+
--direction <value> 历史方向:in|out|all
|
|
817
|
+
--return <required|none> 跨会话回流策略(跨会话默认 required)
|
|
818
|
+
--content-type <mime> 显式覆盖 MIME(仅 --file 模式)
|
|
819
|
+
--as <type> 附件类型:image|video|voice|file(仅 --file 模式)
|
|
820
|
+
--text <说明> 附件说明文字(仅 --file 模式)
|
|
821
|
+
--transcript <text> 语音转写(仅 --as voice)
|
|
822
|
+
--title <text> 链接标题(仅 --link 模式)
|
|
823
|
+
--description <text> 链接描述(仅 --link 模式)
|
|
824
|
+
-- end-of-options:其后所有参数按正文处理
|
|
825
|
+
(用于发送恰好等于某 flag 的文本,如 send a b -- --encrypt)
|
|
826
|
+
|
|
827
|
+
示例:
|
|
828
|
+
ec msg send alice.agentid.pub bob.agentid.pub "hello"
|
|
829
|
+
ec msg send alice.agentid.pub bob.agentid.pub --text-from-file long-message.txt
|
|
830
|
+
ec msg send alice.agentid.pub bob.agentid.pub "讨论项目A" --thread "project-A"
|
|
831
|
+
ec msg send alice.agentid.pub bob.agentid.pub --file ./pic.png
|
|
832
|
+
ec msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video
|
|
833
|
+
ec msg send alice.agentid.pub bob.agentid.pub --link https://example.com --title "AUN"
|
|
834
|
+
ec msg pull alice.agentid.pub --app my-bot
|
|
835
|
+
ec msg ack alice.agentid.pub 42 --app my-bot
|
|
836
|
+
ec msg recall alice.agentid.pub msg-uuid-1 msg-uuid-2
|
|
837
|
+
ec msg online alice.agentid.pub bob.agentid.pub carol.agentid.pub
|
|
838
|
+
ec msg history alice.agentid.pub bob.agentid.pub --session latest --limit 50`);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (sub === 'history' && wantsHelp(args)) {
|
|
842
|
+
console.log(`用法: ec msg history <self-aid> <target-aid> [options]
|
|
843
|
+
|
|
844
|
+
查询指定 AID 对的本地 messages.jsonl,不调用 message.pull,也不影响收件箱游标。
|
|
845
|
+
|
|
846
|
+
Options:
|
|
847
|
+
--session <latest|all|id> 默认 latest;all 查询全部;也可指定会话 ID
|
|
848
|
+
--limit <N> 返回最新 N 条(默认 50,最大 500)
|
|
849
|
+
--before <time> 开区间;epoch 毫秒或 ISO 时间
|
|
850
|
+
--after <time> 开区间;epoch 毫秒或 ISO 时间
|
|
851
|
+
--direction <value> in|out|all(默认 all)
|
|
852
|
+
--format <text|json> 输出格式(默认 text)
|
|
853
|
+
|
|
854
|
+
示例:
|
|
855
|
+
ec msg history alice.agentid.pub bob.agentid.pub
|
|
856
|
+
ec msg history alice.agentid.pub bob.agentid.pub --session latest
|
|
857
|
+
ec msg history alice.agentid.pub bob.agentid.pub --session all
|
|
858
|
+
ec msg history alice.agentid.pub bob.agentid.pub --session meta_20260713_1783920000000 --format json`);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const from = args[1];
|
|
862
|
+
if (!from) {
|
|
863
|
+
console.error('❌ 缺少 <from-aid> 参数');
|
|
864
|
+
process.exit(1);
|
|
865
|
+
}
|
|
866
|
+
const { isValidAid } = await import('../aun/aid/index.js');
|
|
867
|
+
if (!isValidAid(from)) {
|
|
868
|
+
console.error(`❌ 无效 AID 格式: ${from}`);
|
|
869
|
+
process.exit(1);
|
|
870
|
+
}
|
|
871
|
+
const { msgSend, msgPull, msgAck, msgRecall, msgOnline, msgHistory } = await import('../aun/msg/index.js');
|
|
872
|
+
const commonOpts = { aunPath, slotId: appSlot };
|
|
873
|
+
if (sub === 'history') {
|
|
874
|
+
const target = args[2];
|
|
875
|
+
if (!target) {
|
|
876
|
+
console.error('用法: ec msg history <self-aid> <target-aid> [options]');
|
|
877
|
+
process.exit(1);
|
|
878
|
+
}
|
|
879
|
+
const historyValueFlags = new Set(['--session', '--limit', '--before', '--after', '--direction', '--format']);
|
|
880
|
+
const seenHistoryFlags = new Set();
|
|
881
|
+
for (let index = 3; index < args.length; index++) {
|
|
882
|
+
const flag = args[index];
|
|
883
|
+
if (!historyValueFlags.has(flag)) {
|
|
884
|
+
console.error(`❌ 未知 history 参数: ${flag}`);
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
if (seenHistoryFlags.has(flag)) {
|
|
888
|
+
console.error(`❌ history 参数不能重复: ${flag}`);
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
seenHistoryFlags.add(flag);
|
|
892
|
+
const value = args[index + 1];
|
|
893
|
+
if (value === undefined || historyValueFlags.has(value) || value.startsWith('--')) {
|
|
894
|
+
console.error(`❌ ${flag} 缺少参数值`);
|
|
895
|
+
process.exit(1);
|
|
896
|
+
}
|
|
897
|
+
index++;
|
|
898
|
+
}
|
|
899
|
+
const format = getArgValue(args, '--format') ?? 'text';
|
|
900
|
+
if (format !== 'text' && format !== 'json') {
|
|
901
|
+
console.error(`❌ --format 仅支持 text|json: ${format}`);
|
|
902
|
+
process.exit(1);
|
|
903
|
+
}
|
|
904
|
+
const limitRaw = getArgValue(args, '--limit');
|
|
905
|
+
const limit = limitRaw === undefined ? undefined : Number(limitRaw);
|
|
906
|
+
const beforeRaw = getArgValue(args, '--before');
|
|
907
|
+
const afterRaw = getArgValue(args, '--after');
|
|
908
|
+
const before = beforeRaw === undefined ? undefined : parseMsgHistoryTime(beforeRaw);
|
|
909
|
+
const after = afterRaw === undefined ? undefined : parseMsgHistoryTime(afterRaw);
|
|
910
|
+
if (beforeRaw !== undefined && before === undefined) {
|
|
911
|
+
console.error(`❌ --before 必须是 epoch 毫秒或有效时间: ${beforeRaw}`);
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
if (afterRaw !== undefined && after === undefined) {
|
|
915
|
+
console.error(`❌ --after 必须是 epoch 毫秒或有效时间: ${afterRaw}`);
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
const result = msgHistory({
|
|
919
|
+
from,
|
|
920
|
+
target,
|
|
921
|
+
session: getArgValue(args, '--session'),
|
|
922
|
+
limit,
|
|
923
|
+
before,
|
|
924
|
+
after,
|
|
925
|
+
direction: (getArgValue(args, '--direction') ?? 'all'),
|
|
926
|
+
});
|
|
927
|
+
if (!result.ok) {
|
|
928
|
+
if (format === 'json')
|
|
929
|
+
console.log(JSON.stringify(result));
|
|
930
|
+
else
|
|
931
|
+
console.error(`❌ 查询失败: ${result.error}`);
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
if (format === 'json') {
|
|
935
|
+
console.log(JSON.stringify(result));
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (result.count === 0) {
|
|
939
|
+
console.log('暂无消息历史');
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
console.log(`✓ ${result.count} 条消息${result.session_id ? `,session=${result.session_id}` : ''}`);
|
|
943
|
+
for (const message of result.messages) {
|
|
944
|
+
const arrow = message.dir === 'in' ? '←' : '→';
|
|
945
|
+
const peer = message.dir === 'in' ? message.from : message.to;
|
|
946
|
+
const content = message.content.replace(/\r?\n/g, '\n ');
|
|
947
|
+
console.log(` [${message.time}] ${arrow} ${peer} [${message.msgType}] ${content}`);
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (sub === 'send') {
|
|
952
|
+
const to = args[2];
|
|
953
|
+
if (!to) {
|
|
954
|
+
console.error('用法: ec msg send <from> <to> <text|--file ...|--link ...|--payload ...>');
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
if (!isValidAid(to)) {
|
|
958
|
+
console.error(`❌ 无效目标 AID: ${to}`);
|
|
959
|
+
process.exit(1);
|
|
960
|
+
}
|
|
961
|
+
const fileVal = getArgValue(args, '--file');
|
|
962
|
+
const textFromFileVal = getArgValue(args, '--text-from-file');
|
|
963
|
+
const linkVal = getArgValue(args, '--link');
|
|
964
|
+
const payloadVal = getArgValue(args, '--payload');
|
|
965
|
+
let body;
|
|
966
|
+
// 检查互斥参数
|
|
967
|
+
const exclusiveModes = [fileVal, textFromFileVal, linkVal, payloadVal].filter(Boolean);
|
|
968
|
+
if (exclusiveModes.length > 1) {
|
|
969
|
+
console.error('❌ --file, --text-from-file, --link, --payload 互斥,只能指定一个');
|
|
970
|
+
process.exit(1);
|
|
971
|
+
}
|
|
972
|
+
if (fileVal) {
|
|
973
|
+
body = {
|
|
974
|
+
mode: 'file',
|
|
975
|
+
filePath: fileVal,
|
|
976
|
+
as: getArgValue(args, '--as'),
|
|
977
|
+
contentType: getArgValue(args, '--content-type'),
|
|
978
|
+
text: getArgValue(args, '--text'),
|
|
979
|
+
transcript: getArgValue(args, '--transcript'),
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
else if (textFromFileVal) {
|
|
983
|
+
// 从文件读取文本内容
|
|
984
|
+
if (!fs.existsSync(textFromFileVal)) {
|
|
985
|
+
console.error(`❌ 文件不存在: ${textFromFileVal}`);
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
let text;
|
|
989
|
+
try {
|
|
990
|
+
text = fs.readFileSync(textFromFileVal, 'utf-8');
|
|
991
|
+
}
|
|
992
|
+
catch (e) {
|
|
993
|
+
console.error(`❌ 读取文件失败: ${e.message}`);
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}
|
|
996
|
+
if (!text) {
|
|
997
|
+
console.error('❌ 文件内容为空');
|
|
998
|
+
process.exit(1);
|
|
999
|
+
}
|
|
1000
|
+
body = { mode: 'text', text };
|
|
1001
|
+
}
|
|
1002
|
+
else if (linkVal) {
|
|
1003
|
+
body = {
|
|
1004
|
+
mode: 'link',
|
|
1005
|
+
url: linkVal,
|
|
1006
|
+
title: getArgValue(args, '--title'),
|
|
1007
|
+
description: getArgValue(args, '--description'),
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
else if (payloadVal) {
|
|
1011
|
+
let parsed;
|
|
1012
|
+
try {
|
|
1013
|
+
parsed = JSON.parse(payloadVal);
|
|
1014
|
+
}
|
|
1015
|
+
catch (e) {
|
|
1016
|
+
console.error(`❌ --payload 解析失败: ${e.message}`);
|
|
1017
|
+
process.exit(1);
|
|
1018
|
+
}
|
|
1019
|
+
body = { mode: 'payload', payload: parsed };
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
const text = collectPositional(args, 3).join(' ');
|
|
1023
|
+
if (!text) {
|
|
1024
|
+
console.error('❌ 缺少消息内容(文本或 --file/--text-from-file/--link/--payload)');
|
|
1025
|
+
process.exit(1);
|
|
1026
|
+
}
|
|
1027
|
+
body = { mode: 'text', text };
|
|
1028
|
+
}
|
|
1029
|
+
// 加密态:--encrypt 密文,--no-encrypt 明文,都不带默认明文(人类终端直用即此)。
|
|
1030
|
+
// 模型自主发送时由系统提示规则要求按入站消息加密态显式带参。--no-encrypt 优先于 --encrypt。
|
|
1031
|
+
const encrypt = args.includes('--encrypt') && !args.includes('--no-encrypt');
|
|
1032
|
+
const thread = getArgValue(args, '--thread');
|
|
1033
|
+
const returnPolicyRaw = getArgValue(args, '--return');
|
|
1034
|
+
if (returnPolicyRaw && returnPolicyRaw !== 'required' && returnPolicyRaw !== 'none') {
|
|
1035
|
+
console.error(`❌ --return 仅支持 required|none: ${returnPolicyRaw}`);
|
|
1036
|
+
process.exit(1);
|
|
1037
|
+
}
|
|
1038
|
+
// 文件上传进度展示(非 JSON 输出时)。仅在大文件降级到 HTTP PUT 阶段会逐块更新。
|
|
1039
|
+
let lastPctShown = -1;
|
|
1040
|
+
const onUploadProgress = formatJson ? undefined : (info) => {
|
|
1041
|
+
if (info.phase === 'inline')
|
|
1042
|
+
return; // 内联阶段不分块,跳过
|
|
1043
|
+
if (info.phase === 'http-put') {
|
|
1044
|
+
const pct = info.total > 0 ? Math.floor((info.bytes / info.total) * 100) : 0;
|
|
1045
|
+
if (pct === lastPctShown && info.bytes < info.total)
|
|
1046
|
+
return;
|
|
1047
|
+
lastPctShown = pct;
|
|
1048
|
+
const mb = (n) => (n / 1024 / 1024).toFixed(2);
|
|
1049
|
+
const eol = info.bytes >= info.total ? '\n' : '\r';
|
|
1050
|
+
process.stderr.write(` ⏫ uploading: ${pct}% (${mb(info.bytes)}/${mb(info.total)} MB)${eol}`);
|
|
1051
|
+
}
|
|
1052
|
+
else if (info.phase === 'session-create') {
|
|
1053
|
+
process.stderr.write(' ⏫ requesting upload session...\n');
|
|
1054
|
+
}
|
|
1055
|
+
else if (info.phase === 'session-complete') {
|
|
1056
|
+
process.stderr.write(' ⏫ finalizing upload...\n');
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
const result = await msgSend({ from, to, body, encrypt, thread, returnPolicy: returnPolicyRaw, onUploadProgress, ...commonOpts });
|
|
1060
|
+
if (!result.ok) {
|
|
1061
|
+
if (formatJson) {
|
|
1062
|
+
console.log(JSON.stringify(result));
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
console.error(`❌ 发送失败: ${result.error}`);
|
|
1066
|
+
}
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
}
|
|
1069
|
+
if (formatJson) {
|
|
1070
|
+
console.log(JSON.stringify(result));
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
if (result.handoff_id && result.status === 'delivered')
|
|
1074
|
+
console.log(`✓ handoff ${result.handoff_id} 已投递 ${result.message_id ?? ''}`.trimEnd());
|
|
1075
|
+
else if (result.handoff_id)
|
|
1076
|
+
console.log(`✓ 已排队 handoff ${result.handoff_id}(尚未投递)`);
|
|
1077
|
+
else
|
|
1078
|
+
console.log(`✓ 已发送 ${result.message_id ?? ''} seq=${result.seq ?? '-'} status=${result.status ?? '-'}`);
|
|
1079
|
+
}
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (sub === 'pull') {
|
|
1083
|
+
if (!appSlot) {
|
|
1084
|
+
console.warn('⚠ 警告: 未传 --app,当前与 daemon 共享 evolcore 消费通道。pull 会看到/影响 daemon 的消息消费;如需独立消费请用 --app <name>');
|
|
1085
|
+
}
|
|
1086
|
+
const afterSeqStr = getArgValue(args, '--after-seq');
|
|
1087
|
+
const limitStr = getArgValue(args, '--limit');
|
|
1088
|
+
const afterSeq = afterSeqStr !== undefined ? Number(afterSeqStr) : undefined;
|
|
1089
|
+
const limit = limitStr !== undefined ? Number(limitStr) : undefined;
|
|
1090
|
+
if (afterSeq !== undefined && !Number.isFinite(afterSeq)) {
|
|
1091
|
+
console.error(`❌ --after-seq 必须是数字: ${afterSeqStr}`);
|
|
1092
|
+
process.exit(1);
|
|
1093
|
+
}
|
|
1094
|
+
if (limit !== undefined && !Number.isFinite(limit)) {
|
|
1095
|
+
console.error(`❌ --limit 必须是数字: ${limitStr}`);
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
const result = await msgPull({ from, afterSeq, limit, ...commonOpts });
|
|
1099
|
+
if (!result.ok) {
|
|
1100
|
+
if (formatJson) {
|
|
1101
|
+
console.log(JSON.stringify(result));
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
console.error(`❌ 拉取失败: ${result.error}`);
|
|
1105
|
+
}
|
|
1106
|
+
process.exit(1);
|
|
1107
|
+
}
|
|
1108
|
+
if (formatJson) {
|
|
1109
|
+
console.log(JSON.stringify(result));
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
console.log(`✓ ${result.count} 条消息,latest_seq=${result.latest_seq}`);
|
|
1113
|
+
for (const m of result.messages) {
|
|
1114
|
+
const text = m.payload?.text ?? JSON.stringify(m.payload).slice(0, 80);
|
|
1115
|
+
console.log(` [${m.seq}] ${m.from}: ${text}`);
|
|
1116
|
+
}
|
|
1117
|
+
if (result.ephemeral_dropped_count && result.ephemeral_dropped_count > 0) {
|
|
1118
|
+
console.log(` (临时消息淘汰: ${result.ephemeral_dropped_count} 条)`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (sub === 'ack') {
|
|
1124
|
+
const seqStr = args[2];
|
|
1125
|
+
if (!seqStr) {
|
|
1126
|
+
console.error('用法: ec msg ack <from> <seq> [--app <name>]');
|
|
1127
|
+
process.exit(1);
|
|
1128
|
+
}
|
|
1129
|
+
const seq = Number(seqStr);
|
|
1130
|
+
if (!Number.isFinite(seq)) {
|
|
1131
|
+
console.error(`❌ seq 必须是数字: ${seqStr}`);
|
|
1132
|
+
process.exit(1);
|
|
1133
|
+
}
|
|
1134
|
+
if (!appSlot) {
|
|
1135
|
+
console.warn('⚠ 警告: 未传 --app,ack 将推进与 daemon 共享的 evolcore 消费游标,可能影响 daemon 收消息;如需独立请用 --app <name>');
|
|
1136
|
+
}
|
|
1137
|
+
const result = await msgAck({ from, seq, ...commonOpts });
|
|
1138
|
+
if (!result.ok) {
|
|
1139
|
+
if (formatJson) {
|
|
1140
|
+
console.log(JSON.stringify(result));
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
console.error(`❌ ack 失败: ${result.error}`);
|
|
1144
|
+
}
|
|
1145
|
+
process.exit(1);
|
|
1146
|
+
}
|
|
1147
|
+
if (formatJson) {
|
|
1148
|
+
console.log(JSON.stringify(result));
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
console.log(`✓ ack_seq=${result.ack_seq}`);
|
|
1152
|
+
}
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
if (sub === 'recall') {
|
|
1156
|
+
const messageIds = collectPositional(args, 2);
|
|
1157
|
+
if (messageIds.length === 0) {
|
|
1158
|
+
console.error('用法: ec msg recall <from> <message-id> [<message-id>...]');
|
|
1159
|
+
process.exit(1);
|
|
1160
|
+
}
|
|
1161
|
+
const result = await msgRecall({ from, messageIds, ...commonOpts });
|
|
1162
|
+
if (!result.ok) {
|
|
1163
|
+
if (formatJson) {
|
|
1164
|
+
console.log(JSON.stringify(result));
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
console.error(`❌ recall 失败: ${result.error}`);
|
|
1168
|
+
}
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1171
|
+
if (formatJson) {
|
|
1172
|
+
console.log(JSON.stringify(result));
|
|
1173
|
+
}
|
|
1174
|
+
else {
|
|
1175
|
+
console.log(`✓ 受理 ${result.accepted},撤回 ${result.recalled}`);
|
|
1176
|
+
if (result.errors && result.errors.length > 0) {
|
|
1177
|
+
for (const e of result.errors) {
|
|
1178
|
+
console.log(` 失败 ${e.message_id}: ${e.error}`);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (sub === 'online') {
|
|
1185
|
+
const targets = collectPositional(args, 2);
|
|
1186
|
+
if (targets.length === 0) {
|
|
1187
|
+
console.error('用法: ec msg online <from> <target-aid> [<target-aid>...]');
|
|
1188
|
+
process.exit(1);
|
|
1189
|
+
}
|
|
1190
|
+
for (const t of targets) {
|
|
1191
|
+
if (!isValidAid(t)) {
|
|
1192
|
+
console.error(`❌ 无效 AID: ${t}`);
|
|
1193
|
+
process.exit(1);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const result = await msgOnline({ from, targets, ...commonOpts });
|
|
1197
|
+
if (!result.ok) {
|
|
1198
|
+
if (formatJson) {
|
|
1199
|
+
console.log(JSON.stringify(result));
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
console.error(`❌ 查询失败: ${result.error}`);
|
|
1203
|
+
}
|
|
1204
|
+
process.exit(1);
|
|
1205
|
+
}
|
|
1206
|
+
if (formatJson) {
|
|
1207
|
+
console.log(JSON.stringify(result));
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
for (const [aid, online] of Object.entries(result.online)) {
|
|
1211
|
+
console.log(` ${online ? '🟢' : '⚫'} ${aid}`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
console.error(`未知子命令: ${sub}\n用法: ec msg [send|pull|ack|recall|online|history]`);
|
|
1217
|
+
process.exit(1);
|
|
1218
|
+
}
|
|
1219
|
+
// ==================== Group ====================
|
|
1220
|
+
const GROUP_FROM_AID_FORMAT_EXAMPLE = 'sender.example.invalid';
|
|
1221
|
+
const GROUP_AID_FORMAT_EXAMPLE = '12345.example.invalid';
|
|
1222
|
+
function printGroupSendAidHint() {
|
|
1223
|
+
console.error('正确格式: ec group send <发送方AID> <真实群AID> <正文或文件参数>');
|
|
1224
|
+
console.error(`仅格式示例(.example.invalid 是保留的虚构域,不是真实 AID): ec group send ${GROUP_FROM_AID_FORMAT_EXAMPLE} ${GROUP_AID_FORMAT_EXAMPLE} "hello"`);
|
|
1225
|
+
console.error('查询真实群 AID: ec group list <发送方AID>');
|
|
1226
|
+
}
|
|
1227
|
+
export async function cmdGroup(args) {
|
|
1228
|
+
const sub = args[0];
|
|
1229
|
+
const aunPath = resolveAunPath(args);
|
|
1230
|
+
const formatJson = getArgValue(args, '--format') === 'json';
|
|
1231
|
+
const appSlot = getArgValue(args, '--app');
|
|
1232
|
+
const rulesActionHelp = sub === 'rules' && isHelpFlag(args[3]);
|
|
1233
|
+
if (!sub || isHelpFlag(sub) || (wantsHelp(args) && !rulesActionHelp)) {
|
|
1234
|
+
console.log(`用法: ec group <command> <from-aid> [args...] [options]
|
|
1235
|
+
|
|
1236
|
+
消息:
|
|
1237
|
+
send <from> <group-id> <text> 发送群文本
|
|
1238
|
+
send <from> <group-id> --file <path> [--as <type>] 发送群文件
|
|
1239
|
+
send <from> <group-id> --payload <json> 发送自定义 payload
|
|
1240
|
+
pull <from> <group-id> [--after-seq N] [--limit N] 拉取群消息
|
|
1241
|
+
ack <from> <group-id> <seq> [--app <name>] 确认已读
|
|
1242
|
+
|
|
1243
|
+
群管理:
|
|
1244
|
+
create <from> <name> [--visibility public|private] [--description D]
|
|
1245
|
+
[--join-mode open|approval|invite_only|closed] 创建群
|
|
1246
|
+
list <from> [--size N] 列出我加入的群
|
|
1247
|
+
info <from> <group-id> 查看群详情
|
|
1248
|
+
update <from> <group-id> [--name N] [--description D] 修改群信息
|
|
1249
|
+
dissolve <from> <group-id> 解散群
|
|
1250
|
+
suspend <from> <group-id> 暂停群
|
|
1251
|
+
resume <from> <group-id> 恢复群
|
|
1252
|
+
rules <from> <group-id> get 读取已发布群规则文件 /rules.md
|
|
1253
|
+
rules <from> <group-id> set <file> 上传并发布群规则文件
|
|
1254
|
+
rules <from> <group-id> publish 发布当前群空间 /rules.md
|
|
1255
|
+
rules <from> <group-id> [--mode M] [--question Q] [--max-pending N] 查看/更新入群规则
|
|
1256
|
+
|
|
1257
|
+
成员:
|
|
1258
|
+
join <from> <group-id> [--message M] [--answer A] 申请加入
|
|
1259
|
+
leave <from> <group-id> 退出群
|
|
1260
|
+
invite <from> <group-id> <member-aid> [<member-aid>...] 邀请成员
|
|
1261
|
+
kick <from> <group-id> <member-aid> 踢出成员
|
|
1262
|
+
members <from> <group-id> [--page N] [--size N] 列出群成员
|
|
1263
|
+
online <from> <group-id> 查看在线成员
|
|
1264
|
+
role <from> <group-id> <member-aid> <admin|member> 设置成员角色
|
|
1265
|
+
owner <from> <group-id> <new-owner-aid> 转让群主
|
|
1266
|
+
ban <from> <group-id> [<member-aid>] [--duration N] 封禁成员;不带 member-aid 时列出封禁
|
|
1267
|
+
unban <from> <group-id> <member-aid> 解封成员
|
|
1268
|
+
|
|
1269
|
+
Options:
|
|
1270
|
+
--app <name> 指定应用 slot(独立消费通道,不影响 daemon)
|
|
1271
|
+
--aun-path <path> 指定 AUN keystore 根
|
|
1272
|
+
--format json 输出 JSON 格式
|
|
1273
|
+
--encrypt 启用端到端加密(仅 send)
|
|
1274
|
+
--no-encrypt 强制明文发送(优先于 --encrypt;仅 send)
|
|
1275
|
+
--thread <id> 指定话题 ID(仅 send)
|
|
1276
|
+
--return <required|none> 跨会话回流策略(仅 send)
|
|
1277
|
+
--mention <aid> 发送时 @ 某个成员(可多次,或用逗号分隔多个 aid)
|
|
1278
|
+
--mention-all 发送时 @ 所有人
|
|
1279
|
+
--content-type <mime> 显式覆盖 MIME(仅 --file 模式)
|
|
1280
|
+
--as <type> 附件类型:image|video|voice|file(仅 --file 模式)
|
|
1281
|
+
--text <说明> 附件说明文字(仅 --file 模式)
|
|
1282
|
+
--transcript <text> 语音转写(仅 --as voice)
|
|
1283
|
+
--group-id <aid> 指定自定义群 AID(仅 create)
|
|
1284
|
+
-- end-of-options:其后所有参数按正文处理
|
|
1285
|
+
(用于发送恰好等于某 flag 的文本,如 send a g -- --encrypt)
|
|
1286
|
+
|
|
1287
|
+
格式示例(.example.invalid 是保留的虚构域,以下 AID 均不是真实地址):
|
|
1288
|
+
ec group create sender.example.invalid "Dev Team" --visibility private
|
|
1289
|
+
ec group send sender.example.invalid 12345.example.invalid "hello team"
|
|
1290
|
+
ec group send sender.example.invalid 12345.example.invalid "@member 看下 PR" --mention member.example.invalid
|
|
1291
|
+
ec group send sender.example.invalid 12345.example.invalid --file ./arch.png
|
|
1292
|
+
ec group rules sender.example.invalid 12345.example.invalid set ./rules.md
|
|
1293
|
+
ec group invite sender.example.invalid 12345.example.invalid member.example.invalid
|
|
1294
|
+
ec group members sender.example.invalid 12345.example.invalid
|
|
1295
|
+
|
|
1296
|
+
查询真实群 AID:
|
|
1297
|
+
ec group list <发送方AID>`);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
const from = args[1];
|
|
1301
|
+
if (!from) {
|
|
1302
|
+
console.error('❌ 缺少 <from-aid> 参数');
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
}
|
|
1305
|
+
const { isValidAid } = await import('../aun/aid/index.js');
|
|
1306
|
+
if (!isValidAid(from)) {
|
|
1307
|
+
console.error(`❌ 第 1 个位置参数 <from-aid> 无效: ${from}`);
|
|
1308
|
+
if (isExplicitGroupId(from)) {
|
|
1309
|
+
console.error('检测到这里填写的是群 ID;当前命令缺少发送方 AID,后续参数发生了错位。');
|
|
1310
|
+
}
|
|
1311
|
+
printGroupSendAidHint();
|
|
1312
|
+
process.exit(1);
|
|
1313
|
+
}
|
|
1314
|
+
const { groupSend, groupPull, groupAck, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupSuspend, groupResume, groupJoin, groupLeave, groupInvite, groupKick, groupMembers, groupOnline, groupSetRole, groupTransferOwner, groupBan, groupUnban, groupBanlist, groupRules, groupUpdateRules, groupRulesFileGet, groupRulesFileSet, groupRulesFilePublish, } = await import('../aun/msg/index.js');
|
|
1315
|
+
const commonOpts = { aunPath, slotId: appSlot };
|
|
1316
|
+
// 通用 group_id 提取(第三参数)
|
|
1317
|
+
const requireGroupId = () => {
|
|
1318
|
+
const gid = args[2];
|
|
1319
|
+
if (!gid) {
|
|
1320
|
+
console.error('❌ 缺少第 2 个位置参数 <group-aid>');
|
|
1321
|
+
printGroupSendAidHint();
|
|
1322
|
+
process.exit(1);
|
|
1323
|
+
}
|
|
1324
|
+
if (!isExplicitGroupId(gid)) {
|
|
1325
|
+
console.error(`❌ 第 2 个位置参数 <group-aid> 不是有效群 AID: ${gid}`);
|
|
1326
|
+
printGroupSendAidHint();
|
|
1327
|
+
process.exit(1);
|
|
1328
|
+
}
|
|
1329
|
+
return gid;
|
|
1330
|
+
};
|
|
1331
|
+
// 收集 --mention(可多次;每次的值支持逗号分隔多个 aid)
|
|
1332
|
+
const collectMentions = () => {
|
|
1333
|
+
const mentions = [];
|
|
1334
|
+
for (let i = 0; i < args.length; i++) {
|
|
1335
|
+
if (args[i] !== '--mention')
|
|
1336
|
+
continue;
|
|
1337
|
+
const val = args[i + 1];
|
|
1338
|
+
if (val === undefined || val.startsWith('--')) {
|
|
1339
|
+
console.error(`❌ --mention 后面缺少 <aid>`);
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
1342
|
+
for (const aid of val.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
1343
|
+
if (!isValidAid(aid)) {
|
|
1344
|
+
console.error(`❌ --mention 的 aid 无效: ${aid}`);
|
|
1345
|
+
process.exit(1);
|
|
1346
|
+
}
|
|
1347
|
+
mentions.push({ aid });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (args.includes('--mention-all')) {
|
|
1351
|
+
mentions.push({ scope: 'all' });
|
|
1352
|
+
}
|
|
1353
|
+
return mentions;
|
|
1354
|
+
};
|
|
1355
|
+
// 输出辅助
|
|
1356
|
+
const outputResult = (result, successHuman) => {
|
|
1357
|
+
if (!result.ok) {
|
|
1358
|
+
if (formatJson) {
|
|
1359
|
+
console.log(JSON.stringify(result));
|
|
1360
|
+
}
|
|
1361
|
+
else {
|
|
1362
|
+
console.error(`❌ ${result.error}`);
|
|
1363
|
+
}
|
|
1364
|
+
process.exit(1);
|
|
1365
|
+
}
|
|
1366
|
+
if (formatJson) {
|
|
1367
|
+
console.log(JSON.stringify(result));
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
successHuman();
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
const parseNumberFlag = (flag, label) => {
|
|
1374
|
+
const value = getArgValue(args, flag);
|
|
1375
|
+
if (value === undefined)
|
|
1376
|
+
return undefined;
|
|
1377
|
+
if (value.startsWith('--')) {
|
|
1378
|
+
console.error(`❌ ${label} 后面缺少数值`);
|
|
1379
|
+
process.exit(1);
|
|
1380
|
+
}
|
|
1381
|
+
const parsed = Number(value);
|
|
1382
|
+
if (!Number.isFinite(parsed)) {
|
|
1383
|
+
console.error(`❌ ${label} 必须是数字: ${value}`);
|
|
1384
|
+
process.exit(1);
|
|
1385
|
+
}
|
|
1386
|
+
return parsed;
|
|
1387
|
+
};
|
|
1388
|
+
const requireFlagValue = (flag, label) => {
|
|
1389
|
+
const value = getArgValue(args, flag);
|
|
1390
|
+
if (value === undefined || value.startsWith('--')) {
|
|
1391
|
+
console.error(`❌ ${label} 后面缺少值`);
|
|
1392
|
+
process.exit(1);
|
|
1393
|
+
}
|
|
1394
|
+
return value;
|
|
1395
|
+
};
|
|
1396
|
+
const prettyJson = (value, indent = ' ') => {
|
|
1397
|
+
const json = JSON.stringify(value ?? null, null, 2) ?? 'null';
|
|
1398
|
+
return json.split('\n').map(line => indent + line).join('\n');
|
|
1399
|
+
};
|
|
1400
|
+
const renderBanItem = (item) => {
|
|
1401
|
+
if (!item || typeof item !== 'object')
|
|
1402
|
+
return String(item);
|
|
1403
|
+
const data = item;
|
|
1404
|
+
const aid = String(data.aid ?? data.member_aid ?? data.target_aid ?? data.user_id ?? data.peer_id ?? '-');
|
|
1405
|
+
const reason = data.reason ?? data.note ?? data.message ?? data.remark;
|
|
1406
|
+
const until = data.until ?? data.expire_at ?? data.expires_at ?? data.ban_until ?? data.duration_seconds;
|
|
1407
|
+
const parts = [aid];
|
|
1408
|
+
if (until !== undefined && until !== null && until !== '')
|
|
1409
|
+
parts.push(`until=${until}`);
|
|
1410
|
+
if (reason !== undefined && reason !== null && reason !== '')
|
|
1411
|
+
parts.push(`reason=${reason}`);
|
|
1412
|
+
return parts.join(' ');
|
|
1413
|
+
};
|
|
1414
|
+
// ---- 消息 ----
|
|
1415
|
+
if (sub === 'send') {
|
|
1416
|
+
const groupId = requireGroupId();
|
|
1417
|
+
const fileVal = getArgValue(args, '--file');
|
|
1418
|
+
const payloadVal = getArgValue(args, '--payload');
|
|
1419
|
+
let body;
|
|
1420
|
+
if (fileVal) {
|
|
1421
|
+
body = {
|
|
1422
|
+
mode: 'file',
|
|
1423
|
+
filePath: fileVal,
|
|
1424
|
+
as: getArgValue(args, '--as'),
|
|
1425
|
+
contentType: getArgValue(args, '--content-type'),
|
|
1426
|
+
text: getArgValue(args, '--text'),
|
|
1427
|
+
transcript: getArgValue(args, '--transcript'),
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
else if (payloadVal) {
|
|
1431
|
+
let parsed;
|
|
1432
|
+
try {
|
|
1433
|
+
parsed = JSON.parse(payloadVal);
|
|
1434
|
+
}
|
|
1435
|
+
catch (e) {
|
|
1436
|
+
console.error(`❌ --payload 解析失败: ${e.message}`);
|
|
1437
|
+
process.exit(1);
|
|
1438
|
+
}
|
|
1439
|
+
body = { mode: 'payload', payload: parsed };
|
|
1440
|
+
}
|
|
1441
|
+
else {
|
|
1442
|
+
const text = collectPositional(args, 3).join(' ');
|
|
1443
|
+
if (!text) {
|
|
1444
|
+
console.error('❌ 缺少消息内容(文本或 --file/--payload)');
|
|
1445
|
+
process.exit(1);
|
|
1446
|
+
}
|
|
1447
|
+
body = { mode: 'text', text };
|
|
1448
|
+
}
|
|
1449
|
+
const mentions = collectMentions();
|
|
1450
|
+
// --encrypt 密文,--no-encrypt 明文,都不带默认明文。--no-encrypt 优先。
|
|
1451
|
+
const encryptGroup = args.includes('--encrypt') && !args.includes('--no-encrypt');
|
|
1452
|
+
const thread = getArgValue(args, '--thread');
|
|
1453
|
+
const returnPolicyRaw = getArgValue(args, '--return');
|
|
1454
|
+
if (returnPolicyRaw && returnPolicyRaw !== 'required' && returnPolicyRaw !== 'none') {
|
|
1455
|
+
console.error(`❌ --return 仅支持 required|none: ${returnPolicyRaw}`);
|
|
1456
|
+
process.exit(1);
|
|
1457
|
+
}
|
|
1458
|
+
const result = await groupSend({
|
|
1459
|
+
from,
|
|
1460
|
+
groupId,
|
|
1461
|
+
body,
|
|
1462
|
+
mentions: mentions.length ? mentions : undefined,
|
|
1463
|
+
encrypt: encryptGroup,
|
|
1464
|
+
thread,
|
|
1465
|
+
returnPolicy: returnPolicyRaw,
|
|
1466
|
+
...commonOpts,
|
|
1467
|
+
});
|
|
1468
|
+
outputResult(result, () => {
|
|
1469
|
+
const r = result;
|
|
1470
|
+
if (r.handoff_id && r.status === 'delivered')
|
|
1471
|
+
console.log(`✓ handoff ${r.handoff_id} 已投递 ${r.message_id ?? ''}`.trimEnd());
|
|
1472
|
+
else if (r.handoff_id)
|
|
1473
|
+
console.log(`✓ 已排队 handoff ${r.handoff_id}(尚未投递)`);
|
|
1474
|
+
else
|
|
1475
|
+
console.log(`✓ 已发送 message_id=${r.message?.message_id ?? '-'} seq=${r.message?.seq ?? '-'}`);
|
|
1476
|
+
});
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (sub === 'pull') {
|
|
1480
|
+
const groupId = requireGroupId();
|
|
1481
|
+
if (!appSlot) {
|
|
1482
|
+
console.warn('⚠ 警告: 未传 --app,当前与 daemon 共享 evolcore 消费通道。pull 会看到/影响 daemon 的消息消费;如需独立消费请用 --app <name>');
|
|
1483
|
+
}
|
|
1484
|
+
const afterSeq = parseNumberFlag('--after-seq', '--after-seq');
|
|
1485
|
+
const limit = parseNumberFlag('--limit', '--limit');
|
|
1486
|
+
const result = await groupPull({ from, groupId, afterSeq, limit, ...commonOpts });
|
|
1487
|
+
outputResult(result, () => {
|
|
1488
|
+
const r = result;
|
|
1489
|
+
console.log(`✓ ${r.messages.length} 条消息,latest_seq=${r.latest_message_seq}${r.has_more ? '(还有更多)' : ''}`);
|
|
1490
|
+
for (const m of r.messages) {
|
|
1491
|
+
const text = m.payload?.text ?? JSON.stringify(m.payload).slice(0, 80);
|
|
1492
|
+
console.log(` [${m.seq}] ${m.sender_aid}: ${text}`);
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
if (sub === 'ack') {
|
|
1498
|
+
const groupId = requireGroupId();
|
|
1499
|
+
const seqStr = args[3];
|
|
1500
|
+
if (!seqStr) {
|
|
1501
|
+
console.error('用法: ec group ack <from> <group-id> <seq> [--app <name>]');
|
|
1502
|
+
process.exit(1);
|
|
1503
|
+
}
|
|
1504
|
+
const seq = Number(seqStr);
|
|
1505
|
+
if (!Number.isFinite(seq)) {
|
|
1506
|
+
console.error(`❌ seq 必须是数字: ${seqStr}`);
|
|
1507
|
+
process.exit(1);
|
|
1508
|
+
}
|
|
1509
|
+
if (!appSlot) {
|
|
1510
|
+
console.warn('⚠ 警告: 未传 --app,ack 将推进与 daemon 共享的 evolcore 消费游标,可能影响 daemon 收消息;如需独立请用 --app <name>');
|
|
1511
|
+
}
|
|
1512
|
+
const result = await groupAck({ from, groupId, seq, ...commonOpts });
|
|
1513
|
+
outputResult(result, () => {
|
|
1514
|
+
const r = result;
|
|
1515
|
+
console.log(`✓ ack_seq=${r.ack_seq}`);
|
|
1516
|
+
});
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
// ---- 群管理 ----
|
|
1520
|
+
if (sub === 'create') {
|
|
1521
|
+
const name = args[2];
|
|
1522
|
+
if (!name) {
|
|
1523
|
+
console.error('用法: ec group create <from> <name> [--visibility ...] [--description ...]');
|
|
1524
|
+
process.exit(1);
|
|
1525
|
+
}
|
|
1526
|
+
const visibility = getArgValue(args, '--visibility');
|
|
1527
|
+
if (visibility && visibility !== 'public' && visibility !== 'private') {
|
|
1528
|
+
console.error(`❌ --visibility 必须是 public 或 private`);
|
|
1529
|
+
process.exit(1);
|
|
1530
|
+
}
|
|
1531
|
+
const joinMode = getArgValue(args, '--join-mode');
|
|
1532
|
+
if (joinMode && !['open', 'approval', 'invite_only', 'closed'].includes(joinMode)) {
|
|
1533
|
+
console.error('❌ --join-mode 必须是 open|approval|invite_only|closed');
|
|
1534
|
+
process.exit(1);
|
|
1535
|
+
}
|
|
1536
|
+
const result = await groupCreate({
|
|
1537
|
+
from,
|
|
1538
|
+
name,
|
|
1539
|
+
visibility,
|
|
1540
|
+
description: getArgValue(args, '--description'),
|
|
1541
|
+
joinMode: joinMode,
|
|
1542
|
+
groupId: getArgValue(args, '--group-id'),
|
|
1543
|
+
...commonOpts,
|
|
1544
|
+
});
|
|
1545
|
+
outputResult(result, () => {
|
|
1546
|
+
const r = result;
|
|
1547
|
+
console.log(`✓ 已创建群 ${r.group?.group_id}`);
|
|
1548
|
+
console.log(` 名称: ${r.group?.name}`);
|
|
1549
|
+
console.log(` 可见性: ${r.group?.visibility}`);
|
|
1550
|
+
});
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (sub === 'list') {
|
|
1554
|
+
const size = parseNumberFlag('--size', '--size');
|
|
1555
|
+
const result = await groupList({ from, size, ...commonOpts });
|
|
1556
|
+
outputResult(result, () => {
|
|
1557
|
+
const r = result;
|
|
1558
|
+
if (r.items.length === 0) {
|
|
1559
|
+
console.log('(没有加入任何群)');
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
console.log(`共 ${r.total} 个群:`);
|
|
1563
|
+
for (const g of r.items) {
|
|
1564
|
+
console.log(` ${g.group_id} ${g.name} (${g.member_count ?? '?'} 人)`);
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if (sub === 'info') {
|
|
1570
|
+
const groupId = requireGroupId();
|
|
1571
|
+
const result = await groupInfo({ from, groupId, ...commonOpts });
|
|
1572
|
+
outputResult(result, () => {
|
|
1573
|
+
const g = result.group;
|
|
1574
|
+
console.log(`Group: ${g.group_id}`);
|
|
1575
|
+
console.log(` 名称: ${g.name}`);
|
|
1576
|
+
console.log(` 群主: ${g.owner_aid}`);
|
|
1577
|
+
console.log(` 可见性: ${g.visibility ?? '-'}`);
|
|
1578
|
+
console.log(` 状态: ${g.status ?? '-'}`);
|
|
1579
|
+
console.log(` 成员数: ${g.member_count ?? '-'}`);
|
|
1580
|
+
console.log(` 最新 seq: ${g.message_seq ?? '-'}`);
|
|
1581
|
+
if (g.description)
|
|
1582
|
+
console.log(` 描述: ${g.description}`);
|
|
1583
|
+
});
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
if (sub === 'update') {
|
|
1587
|
+
const groupId = requireGroupId();
|
|
1588
|
+
const name = getArgValue(args, '--name');
|
|
1589
|
+
const description = getArgValue(args, '--description');
|
|
1590
|
+
if (name === undefined && description === undefined) {
|
|
1591
|
+
console.error('❌ 至少需要 --name 或 --description 之一');
|
|
1592
|
+
process.exit(1);
|
|
1593
|
+
}
|
|
1594
|
+
const result = await groupUpdate({ from, groupId, name, description, ...commonOpts });
|
|
1595
|
+
outputResult(result, () => {
|
|
1596
|
+
const g = result.group;
|
|
1597
|
+
console.log(`✓ 已更新 ${g.group_id}`);
|
|
1598
|
+
console.log(` 名称: ${g.name}`);
|
|
1599
|
+
});
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
if (sub === 'dissolve') {
|
|
1603
|
+
const groupId = requireGroupId();
|
|
1604
|
+
const result = await groupDissolve({ from, groupId, ...commonOpts });
|
|
1605
|
+
outputResult(result, () => {
|
|
1606
|
+
const r = result;
|
|
1607
|
+
console.log(`✓ 已解散 ${r.group_id} (${r.status})`);
|
|
1608
|
+
});
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
if (sub === 'suspend') {
|
|
1612
|
+
const groupId = requireGroupId();
|
|
1613
|
+
const result = await groupSuspend({ from, groupId, ...commonOpts });
|
|
1614
|
+
outputResult(result, () => {
|
|
1615
|
+
console.log(`✓ 已暂停 ${groupId}`);
|
|
1616
|
+
});
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
if (sub === 'resume') {
|
|
1620
|
+
const groupId = requireGroupId();
|
|
1621
|
+
const result = await groupResume({ from, groupId, ...commonOpts });
|
|
1622
|
+
outputResult(result, () => {
|
|
1623
|
+
console.log(`✓ 已恢复 ${groupId}`);
|
|
1624
|
+
});
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
if (sub === 'rules') {
|
|
1628
|
+
const groupId = requireGroupId();
|
|
1629
|
+
const action = args[3];
|
|
1630
|
+
const printRulesFileUsage = () => {
|
|
1631
|
+
console.log(`用法:
|
|
1632
|
+
ec group rules <from> <group-id> get
|
|
1633
|
+
ec group rules <from> <group-id> set <file>
|
|
1634
|
+
ec group rules <from> <group-id> publish
|
|
1635
|
+
ec group rules <from> <group-id> [--mode open|approval|invite_only|closed] [--question Q] [--max-pending N]
|
|
1636
|
+
|
|
1637
|
+
说明:
|
|
1638
|
+
get 读取当前已发布的群规则文件 /rules.md
|
|
1639
|
+
set 上传本地文件到群空间 /rules.md,发布 rules.content 元数据,并发送 group.rules.updated 通知
|
|
1640
|
+
publish 不上传文件,仅发布当前群空间 /rules.md 的元数据并发送通知
|
|
1641
|
+
|
|
1642
|
+
兼容:
|
|
1643
|
+
不带 get/set/publish 时仍使用旧的入群规则接口。`);
|
|
1644
|
+
};
|
|
1645
|
+
const printRulesFileState = (result, verb) => {
|
|
1646
|
+
if (!result.ok) {
|
|
1647
|
+
if (formatJson) {
|
|
1648
|
+
console.log(JSON.stringify(result));
|
|
1649
|
+
}
|
|
1650
|
+
else {
|
|
1651
|
+
console.error(`❌ ${result.error}`);
|
|
1652
|
+
}
|
|
1653
|
+
process.exit(1);
|
|
1654
|
+
}
|
|
1655
|
+
if (formatJson) {
|
|
1656
|
+
console.log(JSON.stringify(result));
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (verb === 'get' && result.status === 'ok') {
|
|
1660
|
+
process.stdout.write(String(result.content ?? ''));
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (result.status === 'ok') {
|
|
1664
|
+
console.log(`✓ 群规则已发布 ${result.group_id}:/rules.md`);
|
|
1665
|
+
}
|
|
1666
|
+
else {
|
|
1667
|
+
console.log(`群规则状态: ${result.status}`);
|
|
1668
|
+
}
|
|
1669
|
+
if (result.metadata) {
|
|
1670
|
+
console.log(` 元数据: path=${result.metadata.path} size=${result.metadata.size} mtimeMs=${result.metadata.mtimeMs}`);
|
|
1671
|
+
}
|
|
1672
|
+
if (result.remote) {
|
|
1673
|
+
console.log(` 远端: size=${result.remote.size ?? '-'} mtimeMs=${result.remote.mtimeMs ?? '-'}`);
|
|
1674
|
+
}
|
|
1675
|
+
if (result.group_index_etag) {
|
|
1676
|
+
console.log(` group.index etag: ${result.group_index_etag}`);
|
|
1677
|
+
}
|
|
1678
|
+
if (result.notice) {
|
|
1679
|
+
if (result.notice.ok)
|
|
1680
|
+
console.log(` 通知: 已发送${result.notice.message_id ? ` message_id=${result.notice.message_id}` : ''}`);
|
|
1681
|
+
else
|
|
1682
|
+
console.warn(` 通知: 发送失败 ${result.notice.error ?? ''}`.trimEnd());
|
|
1683
|
+
}
|
|
1684
|
+
if (result.error) {
|
|
1685
|
+
console.log(` 错误: ${result.error}`);
|
|
1686
|
+
}
|
|
1687
|
+
if ((verb === 'set' || verb === 'publish') && result.status !== 'ok') {
|
|
1688
|
+
process.exit(1);
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
if (action && isHelpFlag(action)) {
|
|
1692
|
+
printRulesFileUsage();
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
if (action === 'get') {
|
|
1696
|
+
const result = await groupRulesFileGet({ from, groupId, ...commonOpts });
|
|
1697
|
+
printRulesFileState(result, 'get');
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
if (action === 'set') {
|
|
1701
|
+
const filePath = args[4];
|
|
1702
|
+
if (!filePath || filePath.startsWith('--')) {
|
|
1703
|
+
console.error('❌ 缺少 <file> 参数');
|
|
1704
|
+
printRulesFileUsage();
|
|
1705
|
+
process.exit(1);
|
|
1706
|
+
}
|
|
1707
|
+
const result = await groupRulesFileSet({ from, groupId, filePath, ...commonOpts });
|
|
1708
|
+
printRulesFileState(result, 'set');
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
if (action === 'publish') {
|
|
1712
|
+
const result = await groupRulesFilePublish({ from, groupId, ...commonOpts });
|
|
1713
|
+
printRulesFileState(result, 'publish');
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
if (action && !action.startsWith('--')) {
|
|
1717
|
+
console.error(`❌ 未知 rules 动作: ${action}`);
|
|
1718
|
+
printRulesFileUsage();
|
|
1719
|
+
process.exit(1);
|
|
1720
|
+
}
|
|
1721
|
+
const wantsUpdate = args.includes('--mode') || args.includes('--question') || args.includes('--max-pending');
|
|
1722
|
+
if (!wantsUpdate) {
|
|
1723
|
+
const result = await groupRules({ from, groupId, ...commonOpts });
|
|
1724
|
+
outputResult(result, () => {
|
|
1725
|
+
const r = result;
|
|
1726
|
+
console.log(`Rules: ${r.group_id}`);
|
|
1727
|
+
console.log(prettyJson(r.rules));
|
|
1728
|
+
});
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
const modeRaw = args.includes('--mode') ? requireFlagValue('--mode', '--mode') : undefined;
|
|
1732
|
+
const mode = modeRaw ? modeRaw.toLowerCase() : undefined;
|
|
1733
|
+
if (mode && !['open', 'approval', 'invite_only', 'closed'].includes(mode)) {
|
|
1734
|
+
console.error('❌ --mode 必须是 open|approval|invite_only|closed');
|
|
1735
|
+
process.exit(1);
|
|
1736
|
+
}
|
|
1737
|
+
const question = args.includes('--question') ? requireFlagValue('--question', '--question') : undefined;
|
|
1738
|
+
const maxPending = parseNumberFlag('--max-pending', '--max-pending');
|
|
1739
|
+
const result = await groupUpdateRules({
|
|
1740
|
+
from,
|
|
1741
|
+
groupId,
|
|
1742
|
+
mode: mode,
|
|
1743
|
+
question,
|
|
1744
|
+
maxPending,
|
|
1745
|
+
...commonOpts,
|
|
1746
|
+
});
|
|
1747
|
+
outputResult(result, () => {
|
|
1748
|
+
const r = result;
|
|
1749
|
+
console.log(`✓ 已更新规则 ${r.group_id}`);
|
|
1750
|
+
console.log(prettyJson(r.rules));
|
|
1751
|
+
});
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
// ---- 成员 ----
|
|
1755
|
+
if (sub === 'join') {
|
|
1756
|
+
const groupId = requireGroupId();
|
|
1757
|
+
const result = await groupJoin({
|
|
1758
|
+
from, groupId,
|
|
1759
|
+
message: getArgValue(args, '--message'),
|
|
1760
|
+
answer: getArgValue(args, '--answer'),
|
|
1761
|
+
...commonOpts,
|
|
1762
|
+
});
|
|
1763
|
+
outputResult(result, () => {
|
|
1764
|
+
console.log(`✓ 已提交入群申请`);
|
|
1765
|
+
});
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
if (sub === 'leave') {
|
|
1769
|
+
const groupId = requireGroupId();
|
|
1770
|
+
const result = await groupLeave({ from, groupId, ...commonOpts });
|
|
1771
|
+
outputResult(result, () => {
|
|
1772
|
+
console.log(`✓ 已退出 ${groupId}`);
|
|
1773
|
+
});
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
if (sub === 'invite') {
|
|
1777
|
+
const groupId = requireGroupId();
|
|
1778
|
+
const members = collectPositional(args, 3);
|
|
1779
|
+
if (members.length === 0) {
|
|
1780
|
+
console.error('用法: ec group invite <from> <group-id> <member-aid> [<member-aid>...]');
|
|
1781
|
+
process.exit(1);
|
|
1782
|
+
}
|
|
1783
|
+
for (const m of members) {
|
|
1784
|
+
if (!isValidAid(m)) {
|
|
1785
|
+
console.error(`❌ 无效 AID: ${m}`);
|
|
1786
|
+
process.exit(1);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
const result = await groupInvite({ from, groupId, members, ...commonOpts });
|
|
1790
|
+
outputResult(result, () => {
|
|
1791
|
+
const r = result;
|
|
1792
|
+
console.log(`✓ 成功 ${r.added.length},失败 ${r.failed.length}`);
|
|
1793
|
+
for (const a of r.added)
|
|
1794
|
+
console.log(` + ${a}`);
|
|
1795
|
+
for (const f of r.failed)
|
|
1796
|
+
console.log(` ✗ ${f.aid}: ${f.error}`);
|
|
1797
|
+
});
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
if (sub === 'kick') {
|
|
1801
|
+
const groupId = requireGroupId();
|
|
1802
|
+
const memberAid = args[3];
|
|
1803
|
+
if (!memberAid) {
|
|
1804
|
+
console.error('用法: ec group kick <from> <group-id> <member-aid>');
|
|
1805
|
+
process.exit(1);
|
|
1806
|
+
}
|
|
1807
|
+
const result = await groupKick({ from, groupId, memberAid, ...commonOpts });
|
|
1808
|
+
outputResult(result, () => {
|
|
1809
|
+
console.log(`✓ 已踢出 ${memberAid}`);
|
|
1810
|
+
});
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
if (sub === 'members') {
|
|
1814
|
+
const groupId = requireGroupId();
|
|
1815
|
+
const result = await groupMembers({
|
|
1816
|
+
from, groupId,
|
|
1817
|
+
page: parseNumberFlag('--page', '--page'),
|
|
1818
|
+
size: parseNumberFlag('--size', '--size'),
|
|
1819
|
+
...commonOpts,
|
|
1820
|
+
});
|
|
1821
|
+
outputResult(result, () => {
|
|
1822
|
+
const r = result;
|
|
1823
|
+
console.log(`共 ${r.total} 名成员(第 ${r.page} 页):`);
|
|
1824
|
+
for (const m of r.members) {
|
|
1825
|
+
console.log(` [${m.role}] ${m.aid}`);
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
if (sub === 'online') {
|
|
1831
|
+
const groupId = requireGroupId();
|
|
1832
|
+
const result = await groupOnline({ from, groupId, ...commonOpts });
|
|
1833
|
+
outputResult(result, () => {
|
|
1834
|
+
const r = result;
|
|
1835
|
+
console.log(`在线 ${r.online_count}/${r.total}:`);
|
|
1836
|
+
for (const m of r.members) {
|
|
1837
|
+
console.log(` 🟢 ${m.aid}`);
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
if (sub === 'role') {
|
|
1843
|
+
const groupId = requireGroupId();
|
|
1844
|
+
const values = collectPositional(args, 3);
|
|
1845
|
+
if (values.length !== 2) {
|
|
1846
|
+
console.error('用法: ec group role <from> <group-id> <member-aid> <admin|member>');
|
|
1847
|
+
process.exit(1);
|
|
1848
|
+
}
|
|
1849
|
+
const [memberAid, roleRaw] = values;
|
|
1850
|
+
if (!isValidAid(memberAid)) {
|
|
1851
|
+
console.error(`❌ 无效 AID: ${memberAid}`);
|
|
1852
|
+
process.exit(1);
|
|
1853
|
+
}
|
|
1854
|
+
const role = roleRaw.toLowerCase();
|
|
1855
|
+
if (role !== 'admin' && role !== 'member') {
|
|
1856
|
+
console.error('❌ 角色必须是 admin 或 member');
|
|
1857
|
+
process.exit(1);
|
|
1858
|
+
}
|
|
1859
|
+
const result = await groupSetRole({ from, groupId, memberAid, role: role, ...commonOpts });
|
|
1860
|
+
outputResult(result, () => {
|
|
1861
|
+
console.log(`✓ 已将 ${memberAid} 设为 ${role}`);
|
|
1862
|
+
});
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
if (sub === 'owner') {
|
|
1866
|
+
const groupId = requireGroupId();
|
|
1867
|
+
const values = collectPositional(args, 3);
|
|
1868
|
+
if (values.length !== 1) {
|
|
1869
|
+
console.error('用法: ec group owner <from> <group-id> <new-owner-aid>');
|
|
1870
|
+
process.exit(1);
|
|
1871
|
+
}
|
|
1872
|
+
const [newOwner] = values;
|
|
1873
|
+
if (!isValidAid(newOwner)) {
|
|
1874
|
+
console.error(`❌ 无效 AID: ${newOwner}`);
|
|
1875
|
+
process.exit(1);
|
|
1876
|
+
}
|
|
1877
|
+
const result = await groupTransferOwner({ from, groupId, newOwner, ...commonOpts });
|
|
1878
|
+
outputResult(result, () => {
|
|
1879
|
+
const data = result.data ?? {};
|
|
1880
|
+
if (data.status === 'pending_rekey') {
|
|
1881
|
+
console.log(`✓ 已发起群主转让,等待 ${newOwner} 完成 rekey`);
|
|
1882
|
+
if (data.complete_error)
|
|
1883
|
+
console.log(` 自动完成失败: ${data.complete_error}`);
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
console.log(`✓ 已将群主转让给 ${newOwner}${data.auto_completed ? '(已自动完成 rekey)' : ''}`);
|
|
1887
|
+
});
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (sub === 'ban') {
|
|
1891
|
+
const groupId = requireGroupId();
|
|
1892
|
+
const values = collectPositional(args, 3, new Set(['--duration']));
|
|
1893
|
+
if (values.length === 0) {
|
|
1894
|
+
const result = await groupBanlist({ from, groupId, ...commonOpts });
|
|
1895
|
+
outputResult(result, () => {
|
|
1896
|
+
const r = result;
|
|
1897
|
+
if (r.items.length === 0) {
|
|
1898
|
+
console.log('(没有封禁成员)');
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
console.log(`共 ${r.items.length} 条封禁:`);
|
|
1902
|
+
for (const item of r.items) {
|
|
1903
|
+
console.log(` ${renderBanItem(item)}`);
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (values.length !== 1) {
|
|
1909
|
+
console.error('用法: ec group ban <from> <group-id> [<member-aid>] [--duration <seconds>]');
|
|
1910
|
+
process.exit(1);
|
|
1911
|
+
}
|
|
1912
|
+
const [memberAid] = values;
|
|
1913
|
+
if (!isValidAid(memberAid)) {
|
|
1914
|
+
console.error(`❌ 无效 AID: ${memberAid}`);
|
|
1915
|
+
process.exit(1);
|
|
1916
|
+
}
|
|
1917
|
+
const durationSeconds = parseNumberFlag('--duration', '--duration');
|
|
1918
|
+
const result = await groupBan({ from, groupId, memberAid, durationSeconds, ...commonOpts });
|
|
1919
|
+
outputResult(result, () => {
|
|
1920
|
+
console.log(`✓ 已封禁 ${memberAid}${durationSeconds !== undefined ? ` (${durationSeconds}s)` : ''}`);
|
|
1921
|
+
});
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
if (sub === 'unban') {
|
|
1925
|
+
const groupId = requireGroupId();
|
|
1926
|
+
const values = collectPositional(args, 3);
|
|
1927
|
+
if (values.length !== 1) {
|
|
1928
|
+
console.error('用法: ec group unban <from> <group-id> <member-aid>');
|
|
1929
|
+
process.exit(1);
|
|
1930
|
+
}
|
|
1931
|
+
const [memberAid] = values;
|
|
1932
|
+
if (!isValidAid(memberAid)) {
|
|
1933
|
+
console.error(`❌ 无效 AID: ${memberAid}`);
|
|
1934
|
+
process.exit(1);
|
|
1935
|
+
}
|
|
1936
|
+
const result = await groupUnban({ from, groupId, memberAid, ...commonOpts });
|
|
1937
|
+
outputResult(result, () => {
|
|
1938
|
+
console.log(`✓ 已解封 ${memberAid}`);
|
|
1939
|
+
});
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
console.error(`未知子命令: ${sub}\n用法: ec group [send|pull|ack|create|list|info|update|dissolve|suspend|resume|rules|join|leave|invite|kick|members|online|role|owner|ban|unban]`);
|
|
1943
|
+
process.exit(1);
|
|
1944
|
+
}
|
|
1945
|
+
// ==================== Main ====================
|
|
1946
|
+
/**
|
|
1947
|
+
* 收集位置参数(从 startIdx 开始)。
|
|
1948
|
+
*
|
|
1949
|
+
* flag 判定采用**精确匹配已知 flag 集合**,而非 `startsWith('--')`——
|
|
1950
|
+
* 这样"正文恰好以 -- 开头"(如消息文本 `--file 坏了`)不会被误当 flag 吞掉。
|
|
1951
|
+
* 仅当 token 精确等于某个已知 flag 时才按 flag 处理:
|
|
1952
|
+
* - VALUE_FLAGS:消耗自身 + 下一个 arg(flag 的值)
|
|
1953
|
+
* - BOOLEAN_FLAGS:仅消耗自身
|
|
1954
|
+
* 其余以 -- 开头但不在集合中的 token,一律视为正文。
|
|
1955
|
+
*
|
|
1956
|
+
* 另支持 POSIX `--` end-of-options 分隔符:遇到单独的 `--` 后,
|
|
1957
|
+
* 其后所有 token 无条件按正文处理(用于发送精确等于某 flag 的文本,如 `-- --encrypt`)。
|
|
1958
|
+
*/
|
|
1959
|
+
const VALUE_FLAGS = new Set([
|
|
1960
|
+
'--format', '--app', '--after-seq', '--limit', '--file', '--link',
|
|
1961
|
+
'--payload', '--title', '--description', '--text', '--transcript',
|
|
1962
|
+
'--as', '--content-type', '--mention', '--visibility', '--join-mode',
|
|
1963
|
+
'--group-id', '--name', '--message', '--answer', '--page', '--size',
|
|
1964
|
+
'--aun-path', '--thread', '--return',
|
|
1965
|
+
]);
|
|
1966
|
+
const BOOLEAN_FLAGS = new Set([
|
|
1967
|
+
'--encrypt', '--no-encrypt', '--mention-all',
|
|
1968
|
+
]);
|
|
1969
|
+
function collectPositional(args, startIdx, extraValueFlags) {
|
|
1970
|
+
const out = [];
|
|
1971
|
+
let endOfFlags = false;
|
|
1972
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
1973
|
+
const a = args[i];
|
|
1974
|
+
if (endOfFlags) {
|
|
1975
|
+
out.push(a);
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
if (a === '--') {
|
|
1979
|
+
endOfFlags = true;
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
if (VALUE_FLAGS.has(a) || extraValueFlags?.has(a)) {
|
|
1983
|
+
i++;
|
|
1984
|
+
continue;
|
|
1985
|
+
} // 精确匹配取值 flag:跳过其值
|
|
1986
|
+
if (BOOLEAN_FLAGS.has(a)) {
|
|
1987
|
+
continue;
|
|
1988
|
+
} // 精确匹配开关 flag:仅跳过自身
|
|
1989
|
+
out.push(a); // 其余(含以 -- 开头的未知 token)= 正文
|
|
1990
|
+
}
|
|
1991
|
+
return out;
|
|
1992
|
+
}
|
|
1993
|
+
function parseMsgHistoryTime(value) {
|
|
1994
|
+
const trimmed = value.trim();
|
|
1995
|
+
if (!trimmed)
|
|
1996
|
+
return undefined;
|
|
1997
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
1998
|
+
const timestamp = Number(trimmed);
|
|
1999
|
+
return Number.isSafeInteger(timestamp) ? timestamp : undefined;
|
|
2000
|
+
}
|
|
2001
|
+
const timestamp = Date.parse(trimmed);
|
|
2002
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
2003
|
+
}
|