evolcore 0.0.1 → 0.0.2
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 +813 -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/assets/brand/evolcore/README.md +19 -0
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +13 -0
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +126 -0
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +14 -0
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +14 -0
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +10 -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 +2385 -0
- package/dist/agents/codex-app-server-client.js +448 -0
- package/dist/agents/codex-runner.js +2639 -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 +160 -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 +4147 -0
- package/dist/channels/daemon.js +422 -0
- package/dist/channels/dingtalk.js +649 -0
- package/dist/channels/feishu.js +1789 -0
- package/dist/channels/qqbot.js +409 -0
- package/dist/channels/wechat.js +817 -0
- package/dist/channels/wecom.js +565 -0
- package/dist/cli/agent-command.js +641 -0
- package/dist/cli/agent.js +1059 -0
- package/dist/cli/aun-commands.js +1948 -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/ctl-command.js +62 -0
- package/dist/cli/daemon-commands.js +2887 -0
- package/dist/cli/fs-command.js +1447 -0
- package/dist/cli/handoff-command.js +302 -0
- package/dist/cli/help.js +31 -0
- package/dist/cli/index.js +358 -0
- package/dist/cli/init-channel.js +1377 -0
- package/dist/cli/init.js +553 -0
- package/dist/cli/link-rules.js +240 -0
- package/dist/cli/model.js +590 -0
- package/dist/cli/net-check.js +723 -0
- package/dist/cli/queue-command.js +126 -0
- package/dist/cli/response.js +345 -0
- package/dist/cli/restart-monitor.js +456 -0
- package/dist/cli/stats.js +607 -0
- package/dist/cli/task-context.js +80 -0
- package/dist/cli/trigger-command.js +505 -0
- package/dist/cli/version.js +88 -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 +30 -0
- package/dist/config/builtin-roles.js +85 -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 +916 -0
- package/dist/config/config-operation-service.js +384 -0
- package/dist/config/contact-book.js +371 -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 +139 -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-schema.js +105 -0
- package/dist/config/role-service.js +159 -0
- package/dist/config/role-store.js +204 -0
- package/dist/config/roles.js +55 -0
- package/dist/config/schema-registry.js +154 -0
- package/dist/config/snapshot.js +598 -0
- package/dist/config-store.js +503 -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 +110 -0
- package/dist/core/auth/operation-authorizer.js +718 -0
- package/dist/core/auth/operation-catalog.js +675 -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 +1638 -0
- package/dist/core/command/menu-handler.js +3354 -0
- package/dist/core/command/menu-protocol.js +247 -0
- package/dist/core/command/role-menu.js +1524 -0
- package/dist/core/command/slash-gate.js +148 -0
- package/dist/core/command/slash-handler.js +3056 -0
- package/dist/core/daemon-file-cache.js +216 -0
- package/dist/core/event-bus.js +32 -0
- package/dist/core/event-catalog.js +740 -0
- package/dist/core/evolagent-registry.js +546 -0
- package/dist/core/evolagent.js +331 -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 +657 -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 +803 -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 +1436 -0
- package/dist/core/message/message-utils.js +70 -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 +3776 -0
- package/dist/core/message/response-snapshot.js +83 -0
- package/dist/core/message/stream-debouncer.js +130 -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 +987 -0
- package/dist/core/permission/unix-socket-policy.js +99 -0
- package/dist/core/protected-paths.js +330 -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 +232 -0
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +1587 -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 +2870 -4
- package/dist/ipc.js +748 -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 +151 -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 +291 -0
- package/dist/trigger/parser.js +520 -0
- package/dist/trigger/patch.js +148 -0
- package/dist/trigger/scheduler.js +1600 -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 +621 -0
- package/dist/types.js +12 -0
- package/dist/utils/aid-bind.js +299 -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 +437 -0
- package/dist/utils/locale.js +21 -0
- package/dist/utils/log-writer.js +224 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/markdown-to-plain-text.js +20 -0
- package/dist/utils/media-cache.js +271 -0
- package/dist/utils/model-prices.jsonl +17 -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/stats.js +410 -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 +65 -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 +66 -0
- package/kits/docs/evolcore/agent.md +69 -0
- package/kits/docs/evolcore/aid.md +49 -0
- package/kits/docs/evolcore/config.md +149 -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 +101 -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 +141 -0
- package/kits/docs/evolcore/model.md +47 -0
- package/kits/docs/evolcore/msg.md +130 -0
- package/kits/docs/evolcore/response.md +80 -0
- package/kits/docs/evolcore/rpc.md +35 -0
- package/kits/docs/evolcore/self-summary.md +29 -0
- package/kits/docs/evolcore/stats.md +70 -0
- package/kits/docs/evolcore/storage.md +49 -0
- package/kits/docs/evolcore/trigger.md +524 -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 +191 -0
- package/kits/eck_message_manifest.json +63 -0
- package/kits/migrations/README-role-config-v4.md +32 -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 +29 -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/contact-book.schema.1.json +36 -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/role-config.schema.1.json +200 -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 +8 -0
- package/kits/templates/roles/member.json +40 -0
- package/kits/templates/roles/owner.json +8 -0
- package/kits/templates/roles/visitor.json +39 -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 +26 -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,858 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* command-handler-gateway-control.ts — 网关(baseagent 后端)配置管理。
|
|
3
|
+
*
|
|
4
|
+
* "网关" = baseagent 的 baseUrl + apiKey + model 等接入配置。两级作用域:
|
|
5
|
+
* - 'defaults':agents/defaults.json 的 baseagents 块(全局默认)
|
|
6
|
+
* - <aid>:agents/<aid>/config.json 的 baseagents 块(单 agent 覆盖)
|
|
7
|
+
*
|
|
8
|
+
* 设计要点:
|
|
9
|
+
* - 写入复用既有安全路径:saveDefaultsSafe(自动备份+深合并)/ saveAgent(校验)。
|
|
10
|
+
* - apiKey 永不明文出站:list 时掩码,update 仅接受 "$ENV:NAME" 引用。
|
|
11
|
+
* - test 在 daemon 内用 resolve*Config 解析真实 key 发请求,key 不离开进程。
|
|
12
|
+
* - 写后触发 evolagent.reload 让运行中的 runner 重新解析凭证。
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { resolvePaths } from '../paths.js';
|
|
17
|
+
import { saveDefaultsSafe, saveAgent } from '../config-store.js';
|
|
18
|
+
import { resolveAnthropicConfig, resolveOpenaiConfig } from '../agents/baseagent.js';
|
|
19
|
+
import { resolvePriceRow } from '../stats/billing.js';
|
|
20
|
+
import { ipcQuery } from '../ipc.js';
|
|
21
|
+
import { logger } from '../utils/logger.js';
|
|
22
|
+
/** 已知 baseagent 类型(网关可管理范围)。 */
|
|
23
|
+
const GATEWAY_TYPES = ['claude', 'codex', 'gemini'];
|
|
24
|
+
const DISPLAY_NAMES = {
|
|
25
|
+
claude: 'Claude',
|
|
26
|
+
codex: 'Codex (OpenAI)',
|
|
27
|
+
gemini: 'Gemini',
|
|
28
|
+
};
|
|
29
|
+
// ── 掩码 ──
|
|
30
|
+
const ENV_PREFIX = '$ENV:';
|
|
31
|
+
function maskApiKey(raw) {
|
|
32
|
+
if (typeof raw !== 'string' || !raw)
|
|
33
|
+
return { mask: undefined, isEnvRef: false };
|
|
34
|
+
if (raw.startsWith(ENV_PREFIX))
|
|
35
|
+
return { mask: raw, isEnvRef: true }; // $ENV 引用原样展示(非秘密)
|
|
36
|
+
return { mask: '***', isEnvRef: false }; // 明文:隐藏真实值
|
|
37
|
+
}
|
|
38
|
+
// ── 读取 ──
|
|
39
|
+
function readDefaultsRaw() {
|
|
40
|
+
// loadDefaults 会展开 $ENV,掩码逻辑需看原始引用,故直接读盘
|
|
41
|
+
try {
|
|
42
|
+
const p = path.join(resolvePaths().agentsDir, 'defaults.json');
|
|
43
|
+
if (fs.existsSync(p))
|
|
44
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
logger.warn(`[gateway] read defaults.json failed: ${e}`);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function readAgentRaw(aid) {
|
|
52
|
+
try {
|
|
53
|
+
const p = path.join(resolvePaths().agentsDir, aid, 'config.json');
|
|
54
|
+
if (fs.existsSync(p))
|
|
55
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
logger.warn(`[gateway] read agents/${aid}/config.json failed: ${e}`);
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function listAgentAids() {
|
|
63
|
+
try {
|
|
64
|
+
const dir = resolvePaths().agentsDir;
|
|
65
|
+
if (!fs.existsSync(dir))
|
|
66
|
+
return [];
|
|
67
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
68
|
+
.filter(d => d.isDirectory() && fs.existsSync(path.join(dir, d.name, 'config.json')))
|
|
69
|
+
.map(d => d.name);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function extractEntries(raw, scope, includeEmpty = false) {
|
|
76
|
+
const out = [];
|
|
77
|
+
const ba = raw?.baseagents;
|
|
78
|
+
if (!ba || typeof ba !== 'object') {
|
|
79
|
+
if (!includeEmpty)
|
|
80
|
+
return out;
|
|
81
|
+
for (const type of GATEWAY_TYPES) {
|
|
82
|
+
out.push({
|
|
83
|
+
scope,
|
|
84
|
+
type,
|
|
85
|
+
name: DISPLAY_NAMES[type] ?? type,
|
|
86
|
+
apiKeyIsEnvRef: false,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
for (const type of GATEWAY_TYPES) {
|
|
92
|
+
const c = ba[type];
|
|
93
|
+
if ((!c || typeof c !== 'object') && !includeEmpty)
|
|
94
|
+
continue;
|
|
95
|
+
const cfg = c && typeof c === 'object' ? c : {};
|
|
96
|
+
const { mask, isEnvRef } = maskApiKey(cfg.apiKey);
|
|
97
|
+
out.push({
|
|
98
|
+
scope,
|
|
99
|
+
type,
|
|
100
|
+
name: DISPLAY_NAMES[type] ?? type,
|
|
101
|
+
baseUrl: cfg.baseUrl,
|
|
102
|
+
apiKeyMask: mask,
|
|
103
|
+
apiKeyIsEnvRef: isEnvRef,
|
|
104
|
+
model: cfg.model,
|
|
105
|
+
effort: cfg.effort ?? cfg.reasoning,
|
|
106
|
+
mode: cfg.mode,
|
|
107
|
+
cliPath: cfg.cliPath,
|
|
108
|
+
useVertex: cfg.useVertex,
|
|
109
|
+
project: cfg.project,
|
|
110
|
+
location: cfg.location,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/** gatewayList — 列出全部作用域的网关配置(apiKey 掩码)+ 每个 agent 的 effective 配置(带来源标注)。 */
|
|
116
|
+
export function gatewayList() {
|
|
117
|
+
try {
|
|
118
|
+
const gateways = [];
|
|
119
|
+
const defaults = readDefaultsRaw();
|
|
120
|
+
if (defaults)
|
|
121
|
+
gateways.push(...extractEntries(defaults, 'defaults', true));
|
|
122
|
+
const aids = listAgentAids();
|
|
123
|
+
for (const aid of aids) {
|
|
124
|
+
const raw = readAgentRaw(aid);
|
|
125
|
+
if (raw)
|
|
126
|
+
gateways.push(...extractEntries(raw, aid));
|
|
127
|
+
}
|
|
128
|
+
// 计算每个 agent 的 effective 配置(含来源标注)
|
|
129
|
+
const effective = computeEffective(defaults, aids);
|
|
130
|
+
// 检测环境变量配置与全局配置的差异
|
|
131
|
+
const envMismatch = detectEnvMismatch(defaults, aids);
|
|
132
|
+
return { data: { gateways, scopes: ['defaults', ...aids], types: GATEWAY_TYPES, effective, envMismatch } };
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
logger.error('[gateway] gatewayList 执行出错:', e);
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** 判断字段来源:agent 自己有 → 'agent';defaults 有 → 'defaults';否则 'none'。 */
|
|
140
|
+
function fieldSource(agentBlock, defaultsBlock, key) {
|
|
141
|
+
const agentVal = agentBlock?.[key];
|
|
142
|
+
if (agentVal !== undefined && agentVal !== null && agentVal !== '') {
|
|
143
|
+
return { value: key === 'apiKey' ? maskApiKey(agentVal).mask : String(agentVal), source: 'agent' };
|
|
144
|
+
}
|
|
145
|
+
const defaultsVal = defaultsBlock?.[key];
|
|
146
|
+
if (defaultsVal !== undefined && defaultsVal !== null && defaultsVal !== '') {
|
|
147
|
+
return { value: key === 'apiKey' ? maskApiKey(defaultsVal).mask : String(defaultsVal), source: 'defaults' };
|
|
148
|
+
}
|
|
149
|
+
return { value: undefined, source: 'none' };
|
|
150
|
+
}
|
|
151
|
+
/** 为所有 agent 计算 effective 网关配置。
|
|
152
|
+
* 聚焦每个 agent 的 active_baseagent(实际使用的后端):
|
|
153
|
+
* - agent 自己配了该后端的网关块 → 用 agent 的(标注 'agent')
|
|
154
|
+
* - 没配 → 回落 defaults 对应后端的默认网关(标注 'defaults')
|
|
155
|
+
* active_baseagent 缺失时回落 defaults.active_baseagent,再回落 'claude'。 */
|
|
156
|
+
function computeEffective(defaults, aids) {
|
|
157
|
+
const result = [];
|
|
158
|
+
const defaultsBa = defaults?.baseagents || {};
|
|
159
|
+
const defaultsActive = defaults?.active_baseagent;
|
|
160
|
+
for (const aid of aids) {
|
|
161
|
+
const agentRaw = readAgentRaw(aid);
|
|
162
|
+
const agentBa = agentRaw?.baseagents || {};
|
|
163
|
+
// 该 agent 实际使用的后端
|
|
164
|
+
const activeRaw = agentRaw?.active_baseagent || defaultsActive;
|
|
165
|
+
if (!activeRaw) {
|
|
166
|
+
logger.error(`[GatewayConfig] active_baseagent is empty for aid=${aid}, skipping`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!GATEWAY_TYPES.includes(activeRaw)) {
|
|
170
|
+
logger.error(`[GatewayConfig] unknown baseagent type '${activeRaw}' for aid=${aid}, skipping`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const type = activeRaw;
|
|
174
|
+
const activeSource = agentRaw?.active_baseagent ? 'agent' : 'defaults';
|
|
175
|
+
const dBlock = defaultsBa[type] || {};
|
|
176
|
+
const aBlock = agentBa[type] || {};
|
|
177
|
+
// 整块来源:agent 配了该后端的网关块 → 'agent';否则继承 defaults
|
|
178
|
+
const blockSource = (aBlock && Object.keys(aBlock).length > 0) ? 'agent' : 'defaults';
|
|
179
|
+
const keys = type === 'gemini'
|
|
180
|
+
? ['model', 'apiKey', 'mode', 'cliPath', 'useVertex', 'project', 'location']
|
|
181
|
+
: ['baseUrl', 'apiKey', 'model', 'effort'];
|
|
182
|
+
const fields = {};
|
|
183
|
+
for (const k of keys) {
|
|
184
|
+
fields[k] = fieldSource(aBlock, dBlock, k);
|
|
185
|
+
}
|
|
186
|
+
result.push({
|
|
187
|
+
aid,
|
|
188
|
+
type,
|
|
189
|
+
name: DISPLAY_NAMES[type] ?? type,
|
|
190
|
+
activeBaseagent: type,
|
|
191
|
+
activeSource,
|
|
192
|
+
blockSource,
|
|
193
|
+
fields,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
/** 检测环境变量配置与本地 .env 文件的差异 */
|
|
199
|
+
function detectEnvMismatch(defaults, aids) {
|
|
200
|
+
const result = { hasMismatch: false, mismatches: [] };
|
|
201
|
+
const defaultsBa = defaults?.baseagents || {};
|
|
202
|
+
// 读取全局 .env 文件
|
|
203
|
+
const rootEnvPath = path.join(resolvePaths().root, '.env');
|
|
204
|
+
const rootEnvVars = parseEnvFileSync(rootEnvPath);
|
|
205
|
+
// 标准环境变量映射
|
|
206
|
+
const standardEnvKeys = {
|
|
207
|
+
apiKey: 'ANTHROPIC_AUTH_TOKEN',
|
|
208
|
+
baseUrl: 'ANTHROPIC_BASE_URL',
|
|
209
|
+
};
|
|
210
|
+
// 记录进程环境变量
|
|
211
|
+
const processEnvDebug = {
|
|
212
|
+
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || '(未设置)',
|
|
213
|
+
ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN || '(未设置)',
|
|
214
|
+
};
|
|
215
|
+
// 收集调试信息返回给前端
|
|
216
|
+
result.debug = {
|
|
217
|
+
processEnv: processEnvDebug,
|
|
218
|
+
rootEnvFile: rootEnvVars,
|
|
219
|
+
defaultsConfig: defaultsBa,
|
|
220
|
+
};
|
|
221
|
+
// 检测全局默认配置中的环境变量
|
|
222
|
+
for (const type of GATEWAY_TYPES) {
|
|
223
|
+
const block = defaultsBa[type];
|
|
224
|
+
if (!block)
|
|
225
|
+
continue;
|
|
226
|
+
for (const [field, val] of Object.entries(block)) {
|
|
227
|
+
// 情况1:配置值是 $ENV 引用
|
|
228
|
+
if (typeof val === 'string' && val.startsWith(ENV_PREFIX)) {
|
|
229
|
+
const envVarName = val.slice(ENV_PREFIX.length);
|
|
230
|
+
const fileValue = rootEnvVars[envVarName];
|
|
231
|
+
const processValue = process.env[envVarName];
|
|
232
|
+
// .env 文件中未设置,但进程环境中有值
|
|
233
|
+
if (!fileValue && processValue) {
|
|
234
|
+
result.hasMismatch = true;
|
|
235
|
+
result.mismatches.push({
|
|
236
|
+
aid: 'defaults',
|
|
237
|
+
type,
|
|
238
|
+
field,
|
|
239
|
+
envValue: '(.env 中未设置)',
|
|
240
|
+
configValue: val,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// 情况2:配置值是实际值(非 $ENV 引用),检查是否应该使用环境变量
|
|
245
|
+
else if (typeof val === 'string' && val && standardEnvKeys[field]) {
|
|
246
|
+
const envVarName = standardEnvKeys[field];
|
|
247
|
+
const fileValue = rootEnvVars[envVarName];
|
|
248
|
+
const processValue = process.env[envVarName];
|
|
249
|
+
// 如果进程环境中有值,但配置中的值与进程环境不一致
|
|
250
|
+
if (processValue && val !== processValue) {
|
|
251
|
+
// 同时检查 .env 文件:如果 .env 也没有这个值,说明需要同步
|
|
252
|
+
if (!fileValue || fileValue !== processValue) {
|
|
253
|
+
result.hasMismatch = true;
|
|
254
|
+
result.mismatches.push({
|
|
255
|
+
aid: 'defaults',
|
|
256
|
+
type,
|
|
257
|
+
field,
|
|
258
|
+
envValue: fileValue || '(.env 中未设置)',
|
|
259
|
+
configValue: val,
|
|
260
|
+
envVarName,
|
|
261
|
+
processValue,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// 检测每个 agent 的配置
|
|
269
|
+
for (const aid of aids) {
|
|
270
|
+
const agentRaw = readAgentRaw(aid);
|
|
271
|
+
const agentBa = agentRaw?.baseagents || {};
|
|
272
|
+
// 读取该 agent 的 .env 文件
|
|
273
|
+
const agentEnvPath = path.join(resolvePaths().agentsDir, aid, '.env');
|
|
274
|
+
const agentEnvVars = parseEnvFileSync(agentEnvPath);
|
|
275
|
+
for (const type of GATEWAY_TYPES) {
|
|
276
|
+
const block = agentBa[type];
|
|
277
|
+
if (!block)
|
|
278
|
+
continue;
|
|
279
|
+
for (const [field, val] of Object.entries(block)) {
|
|
280
|
+
// 情况1:配置值是 $ENV 引用
|
|
281
|
+
if (typeof val === 'string' && val.startsWith(ENV_PREFIX)) {
|
|
282
|
+
const envVarName = val.slice(ENV_PREFIX.length);
|
|
283
|
+
const fileValue = agentEnvVars[envVarName];
|
|
284
|
+
const processValue = process.env[envVarName];
|
|
285
|
+
if (!fileValue && processValue) {
|
|
286
|
+
result.hasMismatch = true;
|
|
287
|
+
result.mismatches.push({
|
|
288
|
+
aid,
|
|
289
|
+
type,
|
|
290
|
+
field,
|
|
291
|
+
envValue: '(.env 中未设置)',
|
|
292
|
+
configValue: val,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// 情况2:配置值是实际值
|
|
297
|
+
else if (typeof val === 'string' && val && standardEnvKeys[field]) {
|
|
298
|
+
const envVarName = standardEnvKeys[field];
|
|
299
|
+
const fileValue = agentEnvVars[envVarName];
|
|
300
|
+
const processValue = process.env[envVarName];
|
|
301
|
+
if (processValue && val !== processValue) {
|
|
302
|
+
if (!fileValue || fileValue !== processValue) {
|
|
303
|
+
result.hasMismatch = true;
|
|
304
|
+
result.mismatches.push({
|
|
305
|
+
aid,
|
|
306
|
+
type,
|
|
307
|
+
field,
|
|
308
|
+
envValue: fileValue || '(.env 中未设置)',
|
|
309
|
+
configValue: `配置值与环境变量 ${envVarName} 不一致`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
/** 同步解析 .env 文件(复用 merge.ts 的逻辑) */
|
|
320
|
+
function parseEnvFileSync(file) {
|
|
321
|
+
try {
|
|
322
|
+
const text = fs.readFileSync(file, 'utf-8');
|
|
323
|
+
const out = {};
|
|
324
|
+
for (const line of text.split(/\r?\n/)) {
|
|
325
|
+
const t = line.trim();
|
|
326
|
+
if (!t || t.startsWith('#'))
|
|
327
|
+
continue;
|
|
328
|
+
const eq = t.indexOf('=');
|
|
329
|
+
if (eq <= 0)
|
|
330
|
+
continue;
|
|
331
|
+
const key = t.slice(0, eq).trim();
|
|
332
|
+
let val = t.slice(eq + 1).trim();
|
|
333
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
334
|
+
val = val.slice(1, -1);
|
|
335
|
+
}
|
|
336
|
+
out[key] = val;
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return {};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// ── 写入 ──
|
|
345
|
+
/** 校验 type 合法。 */
|
|
346
|
+
function validateType(type) {
|
|
347
|
+
return typeof type === 'string' && GATEWAY_TYPES.includes(type);
|
|
348
|
+
}
|
|
349
|
+
/** 从 patch 提取允许写入的字段。apiKey 仅接受 $ENV 引用。 */
|
|
350
|
+
function buildPatch(type, patch) {
|
|
351
|
+
const f = {};
|
|
352
|
+
if (patch.baseUrl !== undefined)
|
|
353
|
+
f.baseUrl = String(patch.baseUrl).trim() || undefined;
|
|
354
|
+
if (patch.model !== undefined)
|
|
355
|
+
f.model = String(patch.model).trim() || undefined;
|
|
356
|
+
if (patch.apiKey !== undefined && patch.apiKey !== '') {
|
|
357
|
+
const v = String(patch.apiKey).trim();
|
|
358
|
+
// 掩码占位符(未修改)忽略;其余必须是 $ENV 引用,拒绝明文
|
|
359
|
+
if (v === '***') { /* 未修改,跳过 */ }
|
|
360
|
+
else if (v.startsWith(ENV_PREFIX))
|
|
361
|
+
f.apiKey = v;
|
|
362
|
+
else
|
|
363
|
+
return { error: 'apiKey 仅接受 "$ENV:NAME" 环境变量引用,不接受明文' };
|
|
364
|
+
}
|
|
365
|
+
if (type === 'claude' || type === 'codex') {
|
|
366
|
+
if (patch.effort !== undefined)
|
|
367
|
+
f.effort = String(patch.effort).trim() || undefined;
|
|
368
|
+
}
|
|
369
|
+
if (type === 'codex') {
|
|
370
|
+
if (patch.reasoning !== undefined && patch.effort === undefined)
|
|
371
|
+
f.effort = String(patch.reasoning).trim() || undefined;
|
|
372
|
+
delete f.reasoning;
|
|
373
|
+
}
|
|
374
|
+
if (type === 'gemini') {
|
|
375
|
+
if (patch.mode !== undefined)
|
|
376
|
+
f.mode = (patch.mode === 'sdk' ? 'sdk' : 'cli');
|
|
377
|
+
if (patch.cliPath !== undefined)
|
|
378
|
+
f.cliPath = String(patch.cliPath).trim() || undefined;
|
|
379
|
+
if (patch.useVertex !== undefined)
|
|
380
|
+
f.useVertex = !!patch.useVertex;
|
|
381
|
+
if (patch.project !== undefined)
|
|
382
|
+
f.project = String(patch.project).trim() || undefined;
|
|
383
|
+
if (patch.location !== undefined)
|
|
384
|
+
f.location = String(patch.location).trim() || undefined;
|
|
385
|
+
}
|
|
386
|
+
return { fields: f };
|
|
387
|
+
}
|
|
388
|
+
/** 触发 reload:defaults 改动影响全部 agent(resync),单 agent 改动只 reload 它。 */
|
|
389
|
+
async function triggerReload(scope) {
|
|
390
|
+
const sock = resolvePaths().socket;
|
|
391
|
+
try {
|
|
392
|
+
if (scope === 'defaults') {
|
|
393
|
+
const r = await ipcQuery(sock, { type: 'evolagent.resync' });
|
|
394
|
+
return !!r?.ok;
|
|
395
|
+
}
|
|
396
|
+
const r = await ipcQuery(sock, { type: 'evolagent.reload', name: scope });
|
|
397
|
+
return !!r?.ok;
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/** gatewayUpdate — 写入/更新一条网关配置。 */
|
|
404
|
+
export async function gatewayUpdate(args) {
|
|
405
|
+
const scope = String(args?.scope ?? '').trim();
|
|
406
|
+
const type = args?.type;
|
|
407
|
+
if (!scope)
|
|
408
|
+
return { error: '缺少 scope', code: 'INVALID_ARGS' };
|
|
409
|
+
if (!validateType(type))
|
|
410
|
+
return { error: `无效 type: ${type}(可选 ${GATEWAY_TYPES.join('/')})`, code: 'INVALID_ARGS' };
|
|
411
|
+
const built = buildPatch(type, args?.patch ?? {});
|
|
412
|
+
if ('error' in built)
|
|
413
|
+
return { error: built.error, code: 'INVALID_ARGS' };
|
|
414
|
+
try {
|
|
415
|
+
if (scope === 'defaults') {
|
|
416
|
+
const cur = readDefaultsRaw() ?? {};
|
|
417
|
+
const baseagents = { ...(cur.baseagents ?? {}) };
|
|
418
|
+
baseagents[type] = { ...(baseagents[type] ?? {}), ...built.fields };
|
|
419
|
+
saveDefaultsSafe({ baseagents });
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const cfg = readAgentRaw(scope);
|
|
423
|
+
if (!cfg)
|
|
424
|
+
return { error: `Agent "${scope}" 不存在`, code: 'NOT_FOUND' };
|
|
425
|
+
if (!cfg.baseagents)
|
|
426
|
+
cfg.baseagents = {};
|
|
427
|
+
cfg.baseagents[type] = { ...(cfg.baseagents[type] ?? {}), ...built.fields };
|
|
428
|
+
saveAgent(cfg); // 含 aid/owners/admins 校验
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
return { error: e?.message || String(e), code: 'EXEC_FAILED' };
|
|
433
|
+
}
|
|
434
|
+
const reloaded = await triggerReload(scope);
|
|
435
|
+
return { data: { scope, type, reloaded } };
|
|
436
|
+
}
|
|
437
|
+
/** gatewayDelete — 删除一条网关配置。 */
|
|
438
|
+
export async function gatewayDelete(args) {
|
|
439
|
+
const scope = String(args?.scope ?? '').trim();
|
|
440
|
+
const type = args?.type;
|
|
441
|
+
if (!scope)
|
|
442
|
+
return { error: '缺少 scope', code: 'INVALID_ARGS' };
|
|
443
|
+
if (!validateType(type))
|
|
444
|
+
return { error: `无效 type: ${type}`, code: 'INVALID_ARGS' };
|
|
445
|
+
try {
|
|
446
|
+
if (scope === 'defaults') {
|
|
447
|
+
const cur = readDefaultsRaw();
|
|
448
|
+
if (cur?.baseagents?.[type]) {
|
|
449
|
+
const baseagents = { ...cur.baseagents };
|
|
450
|
+
delete baseagents[type];
|
|
451
|
+
// saveDefaultsSafe 是深合并不会删键,故直接整块覆盖 baseagents
|
|
452
|
+
saveDefaultsSafe({ baseagents });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
const cfg = readAgentRaw(scope);
|
|
457
|
+
if (!cfg)
|
|
458
|
+
return { error: `Agent "${scope}" 不存在`, code: 'NOT_FOUND' };
|
|
459
|
+
if (cfg.baseagents?.[type]) {
|
|
460
|
+
delete cfg.baseagents[type];
|
|
461
|
+
saveAgent(cfg);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
return { error: e?.message || String(e), code: 'EXEC_FAILED' };
|
|
467
|
+
}
|
|
468
|
+
const reloaded = await triggerReload(scope);
|
|
469
|
+
return { data: { scope, type, deleted: true, reloaded } };
|
|
470
|
+
}
|
|
471
|
+
/** 解析某 scope/type 的真实 baseUrl + apiKey(在 daemon 内展开 $ENV,不出站)。 */
|
|
472
|
+
function resolveGatewayCreds(scope, type) {
|
|
473
|
+
const raw = scope === 'defaults' ? readDefaultsRaw() : readAgentRaw(scope);
|
|
474
|
+
const block = raw?.baseagents?.[type];
|
|
475
|
+
if (!block)
|
|
476
|
+
return { error: `${scope}/${type} 未配置`, code: 'NOT_FOUND' };
|
|
477
|
+
const expanded = expandEnv(block);
|
|
478
|
+
try {
|
|
479
|
+
const synth = { agents: { [type]: expanded } };
|
|
480
|
+
if (type === 'codex') {
|
|
481
|
+
const r = resolveOpenaiConfig(synth, expanded);
|
|
482
|
+
return { baseUrl: r.baseUrl, apiKey: r.apiKey };
|
|
483
|
+
}
|
|
484
|
+
else if (type === 'claude') {
|
|
485
|
+
const r = resolveAnthropicConfig(synth, expanded);
|
|
486
|
+
return { baseUrl: r.baseUrl, apiKey: r.apiKey };
|
|
487
|
+
}
|
|
488
|
+
return { error: 'gemini 暂不支持 HTTP 探测(CLI 后端)', code: 'NOT_SUPPORTED' };
|
|
489
|
+
}
|
|
490
|
+
catch (e) {
|
|
491
|
+
return { error: `凭证解析失败:${e?.message || e}`, code: 'EXEC_FAILED' };
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/** gatewayTest — 连通性测试。在 daemon 内解析真实 key(不出站)发 /v1/models。 */
|
|
495
|
+
export async function gatewayTest(args) {
|
|
496
|
+
const scope = String(args?.scope ?? '').trim();
|
|
497
|
+
const type = args?.type;
|
|
498
|
+
if (!validateType(type))
|
|
499
|
+
return { error: `无效 type: ${type}`, code: 'INVALID_ARGS' };
|
|
500
|
+
const creds = resolveGatewayCreds(scope, type);
|
|
501
|
+
if ('error' in creds)
|
|
502
|
+
return creds;
|
|
503
|
+
const { baseUrl, apiKey } = creds;
|
|
504
|
+
if (!baseUrl)
|
|
505
|
+
return { error: '未配置 baseUrl(或为官方占位地址)', code: 'INVALID_ARGS' };
|
|
506
|
+
const start = Date.now();
|
|
507
|
+
try {
|
|
508
|
+
const controller = new AbortController();
|
|
509
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
510
|
+
const resp = await fetch(`${baseUrl.replace(/\/+$/, '')}/v1/models`, {
|
|
511
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
512
|
+
signal: controller.signal,
|
|
513
|
+
});
|
|
514
|
+
clearTimeout(timer);
|
|
515
|
+
const latency = Date.now() - start;
|
|
516
|
+
if (!resp.ok)
|
|
517
|
+
return { data: { ok: false, latency, error: `HTTP ${resp.status}` } };
|
|
518
|
+
const json = await resp.json().catch(() => null);
|
|
519
|
+
const arr = Array.isArray(json?.data) ? json.data
|
|
520
|
+
: Array.isArray(json?.models) ? json.models : [];
|
|
521
|
+
const models = arr.map((m) => (typeof m === 'string' ? m : (m?.id || m?.name || m?.model))).filter(Boolean);
|
|
522
|
+
return { data: { ok: true, latency, modelCount: models.length, models } };
|
|
523
|
+
}
|
|
524
|
+
catch (e) {
|
|
525
|
+
return { data: { ok: false, latency: Date.now() - start, error: e?.message || String(e) } };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/** 从 model-prices.jsonl 的 PriceRecord 提取展示用价格四元组。 */
|
|
529
|
+
function priceRowToQuad(row) {
|
|
530
|
+
if (!row)
|
|
531
|
+
return undefined;
|
|
532
|
+
return {
|
|
533
|
+
input: typeof row.price_input === 'number' ? row.price_input : undefined,
|
|
534
|
+
output: typeof row.price_output === 'number' ? row.price_output : undefined,
|
|
535
|
+
cache_read: typeof row.price_cache_read === 'number' ? row.price_cache_read : undefined,
|
|
536
|
+
cache_write: typeof row.price_cache_creation === 'number' ? row.price_cache_creation : undefined,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
/** 从接口 pricing 对象提取四元组。 */
|
|
540
|
+
function apiPricingToQuad(p) {
|
|
541
|
+
if (!p || typeof p !== 'object')
|
|
542
|
+
return undefined;
|
|
543
|
+
return {
|
|
544
|
+
input: typeof p.input === 'number' ? p.input : undefined,
|
|
545
|
+
output: typeof p.output === 'number' ? p.output : undefined,
|
|
546
|
+
cache_read: typeof p.cache_read === 'number' ? p.cache_read : undefined,
|
|
547
|
+
cache_write: typeof p.cache_write === 'number' ? p.cache_write : undefined,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
/** gatewayModels — 拉取该网关模型列表 + 官方价格 + 网关价格(接口缺失则回退 model-prices.jsonl)。 */
|
|
551
|
+
export async function gatewayModels(args) {
|
|
552
|
+
const scope = String(args?.scope ?? '').trim();
|
|
553
|
+
const type = args?.type;
|
|
554
|
+
if (!validateType(type))
|
|
555
|
+
return { error: `无效 type: ${type}`, code: 'INVALID_ARGS' };
|
|
556
|
+
const creds = resolveGatewayCreds(scope, type);
|
|
557
|
+
if ('error' in creds)
|
|
558
|
+
return creds;
|
|
559
|
+
const { baseUrl, apiKey } = creds;
|
|
560
|
+
if (!baseUrl)
|
|
561
|
+
return { error: '未配置 baseUrl(或为官方占位地址)', code: 'INVALID_ARGS' };
|
|
562
|
+
let arr = [];
|
|
563
|
+
let usdToCny = 7; // 默认汇率
|
|
564
|
+
try {
|
|
565
|
+
const controller = new AbortController();
|
|
566
|
+
const timer = setTimeout(() => controller.abort(), 8000);
|
|
567
|
+
const resp = await fetch(`${baseUrl.replace(/\/+$/, '')}/v1/models`, {
|
|
568
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
569
|
+
signal: controller.signal,
|
|
570
|
+
});
|
|
571
|
+
clearTimeout(timer);
|
|
572
|
+
if (!resp.ok)
|
|
573
|
+
return { error: `HTTP ${resp.status}`, code: 'EXEC_FAILED' };
|
|
574
|
+
const json = await resp.json().catch(() => null);
|
|
575
|
+
arr = Array.isArray(json?.data) ? json.data : Array.isArray(json?.models) ? json.models : [];
|
|
576
|
+
// 提取汇率(usd_to_cny),如果没有则使用默认值 7
|
|
577
|
+
if (typeof json?.usd_to_cny === 'number') {
|
|
578
|
+
usdToCny = json.usd_to_cny;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch (e) {
|
|
582
|
+
return { error: `请求失败:${e?.message || e}`, code: 'EXEC_FAILED' };
|
|
583
|
+
}
|
|
584
|
+
const evolcoreHome = resolvePaths().root;
|
|
585
|
+
const now = Date.now();
|
|
586
|
+
const models = arr.map((m) => {
|
|
587
|
+
const id = typeof m === 'string' ? m : (m?.id || m?.name || m?.model);
|
|
588
|
+
if (!id)
|
|
589
|
+
return null;
|
|
590
|
+
const name = (m && typeof m === 'object' && m.name) ? m.name : id;
|
|
591
|
+
const group = (m && typeof m === 'object' && m.group) ? m.group : undefined;
|
|
592
|
+
// 官方价格:接口 pricing 优先,缺失回退 model-prices.jsonl
|
|
593
|
+
let official = apiPricingToQuad(m?.pricing);
|
|
594
|
+
let officialSource = official ? 'gateway' : 'none';
|
|
595
|
+
if (!official) {
|
|
596
|
+
const row = resolvePriceRow(evolcoreHome, id, now);
|
|
597
|
+
const quad = priceRowToQuad(row);
|
|
598
|
+
if (quad) {
|
|
599
|
+
official = quad;
|
|
600
|
+
officialSource = 'local';
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// 网关价格:接口 effective_pricing;缺失则留空(用户可手动设)
|
|
604
|
+
const gatewayPrice = apiPricingToQuad(m?.effective_pricing);
|
|
605
|
+
return { id, name, group, official: official ?? null, officialSource, gateway: gatewayPrice ?? null };
|
|
606
|
+
}).filter(Boolean);
|
|
607
|
+
return { data: { scope, type, models, usdToCny } };
|
|
608
|
+
}
|
|
609
|
+
/** gatewaySetPrice — 把用户改的网关价格 append 到用户覆盖层 model-prices.jsonl。 */
|
|
610
|
+
export function gatewaySetPrice(args) {
|
|
611
|
+
const modelId = String(args?.modelId ?? '').trim();
|
|
612
|
+
if (!modelId)
|
|
613
|
+
return { error: '缺少 modelId', code: 'INVALID_ARGS' };
|
|
614
|
+
const p = args?.pricing;
|
|
615
|
+
if (!p || typeof p !== 'object')
|
|
616
|
+
return { error: '缺少 pricing', code: 'INVALID_ARGS' };
|
|
617
|
+
const num = (v) => (typeof v === 'number' && isFinite(v) && v >= 0) ? v : undefined;
|
|
618
|
+
const record = {
|
|
619
|
+
model: modelId,
|
|
620
|
+
effective_from: Date.now(),
|
|
621
|
+
billing_fn: 'per_token_v1',
|
|
622
|
+
currency: 'USD',
|
|
623
|
+
};
|
|
624
|
+
if (num(p.input) !== undefined)
|
|
625
|
+
record.price_input = num(p.input);
|
|
626
|
+
if (num(p.output) !== undefined)
|
|
627
|
+
record.price_output = num(p.output);
|
|
628
|
+
if (num(p.cache_read) !== undefined)
|
|
629
|
+
record.price_cache_read = num(p.cache_read);
|
|
630
|
+
if (num(p.cache_write) !== undefined)
|
|
631
|
+
record.price_cache_creation = num(p.cache_write);
|
|
632
|
+
if (record.price_input === undefined && record.price_output === undefined
|
|
633
|
+
&& record.price_cache_read === undefined && record.price_cache_creation === undefined) {
|
|
634
|
+
return { error: '至少需提供一个有效价格字段', code: 'INVALID_ARGS' };
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
const dir = path.join(resolvePaths().root, 'data', 'stats');
|
|
638
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
639
|
+
// 写入网关价格覆盖层(price-resolver 的「优先级1 用户手动设置网关价」读此文件)。
|
|
640
|
+
// 注意:必须是 model-prices-gateway.jsonl,不是 model-prices.jsonl(后者是官方价表)。
|
|
641
|
+
const file = path.join(dir, 'model-prices-gateway.jsonl');
|
|
642
|
+
const mode = 0o600; // owner-only read/write for security
|
|
643
|
+
const fd = fs.openSync(file, 'a', mode);
|
|
644
|
+
try {
|
|
645
|
+
fs.writeSync(fd, JSON.stringify(record) + '\n');
|
|
646
|
+
}
|
|
647
|
+
finally {
|
|
648
|
+
fs.closeSync(fd);
|
|
649
|
+
}
|
|
650
|
+
logger.info(`[gateway] 网关价格已更新: ${modelId}(写入网关价覆盖层 model-prices-gateway.jsonl)`);
|
|
651
|
+
}
|
|
652
|
+
catch (e) {
|
|
653
|
+
return { error: e?.message || String(e), code: 'EXEC_FAILED' };
|
|
654
|
+
}
|
|
655
|
+
return { data: { modelId, saved: true } };
|
|
656
|
+
}
|
|
657
|
+
/** gatewaySyncEnv — 同步环境变量到配置文件(将进程环境变量的值写入配置文件) */
|
|
658
|
+
export async function gatewaySyncEnv(args) {
|
|
659
|
+
const syncType = args?.syncType; // 'global' | 'all-agents' | 'specific-agents'
|
|
660
|
+
const targetAids = args?.targetAids || []; // 指定的 agent IDs(仅 specific-agents 时使用)
|
|
661
|
+
if (!['global', 'all-agents', 'specific-agents'].includes(syncType)) {
|
|
662
|
+
return { error: '无效的 syncType,可选值:global / all-agents / specific-agents', code: 'INVALID_ARGS' };
|
|
663
|
+
}
|
|
664
|
+
if (syncType === 'specific-agents' && (!Array.isArray(targetAids) || targetAids.length === 0)) {
|
|
665
|
+
return { error: 'specific-agents 模式需要提供 targetAids 数组', code: 'INVALID_ARGS' };
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
const synced = [];
|
|
669
|
+
// 1. 同步全局配置
|
|
670
|
+
if (syncType === 'global' || syncType === 'all-agents' || syncType === 'specific-agents') {
|
|
671
|
+
const defaults = readDefaultsRaw();
|
|
672
|
+
if (defaults?.baseagents) {
|
|
673
|
+
let hasChanges = false;
|
|
674
|
+
for (const type of GATEWAY_TYPES) {
|
|
675
|
+
const block = defaults.baseagents[type];
|
|
676
|
+
if (!block)
|
|
677
|
+
continue;
|
|
678
|
+
for (const [field, val] of Object.entries(block)) {
|
|
679
|
+
// 检测配置值是否与进程环境变量不一致
|
|
680
|
+
if (typeof val === 'string') {
|
|
681
|
+
// 情况1:引用型 $ENV:XXX
|
|
682
|
+
if (val.startsWith(ENV_PREFIX)) {
|
|
683
|
+
const varName = val.slice(ENV_PREFIX.length);
|
|
684
|
+
const processValue = process.env[varName];
|
|
685
|
+
if (processValue) {
|
|
686
|
+
// 将引用改为实际值
|
|
687
|
+
block[field] = processValue;
|
|
688
|
+
hasChanges = true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// 情况2:实际值型,但与进程环境变量不一致
|
|
692
|
+
else {
|
|
693
|
+
const standardEnvKey = field === 'apiKey' ? 'ANTHROPIC_AUTH_TOKEN' : field === 'baseUrl' ? 'ANTHROPIC_BASE_URL' : null;
|
|
694
|
+
if (standardEnvKey) {
|
|
695
|
+
const processValue = process.env[standardEnvKey];
|
|
696
|
+
if (processValue && val !== processValue) {
|
|
697
|
+
block[field] = processValue;
|
|
698
|
+
hasChanges = true;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (hasChanges) {
|
|
706
|
+
const defaultsPath = path.join(resolvePaths().agentsDir, 'defaults.json');
|
|
707
|
+
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2), 'utf-8');
|
|
708
|
+
synced.push('全局配置 (defaults.json)');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// 2. 同步 agent 配置
|
|
713
|
+
if (syncType === 'all-agents' || syncType === 'specific-agents') {
|
|
714
|
+
const aids = syncType === 'all-agents' ? listAgentAids() : targetAids;
|
|
715
|
+
for (const aid of aids) {
|
|
716
|
+
const agentRaw = readAgentRaw(aid);
|
|
717
|
+
if (!agentRaw?.baseagents)
|
|
718
|
+
continue;
|
|
719
|
+
let hasChanges = false;
|
|
720
|
+
for (const type of GATEWAY_TYPES) {
|
|
721
|
+
const block = agentRaw.baseagents[type];
|
|
722
|
+
if (!block)
|
|
723
|
+
continue;
|
|
724
|
+
for (const [field, val] of Object.entries(block)) {
|
|
725
|
+
if (typeof val === 'string') {
|
|
726
|
+
// 情况1:引用型
|
|
727
|
+
if (val.startsWith(ENV_PREFIX)) {
|
|
728
|
+
const varName = val.slice(ENV_PREFIX.length);
|
|
729
|
+
const processValue = process.env[varName];
|
|
730
|
+
if (processValue) {
|
|
731
|
+
block[field] = processValue;
|
|
732
|
+
hasChanges = true;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
// 情况2:实际值型
|
|
736
|
+
else {
|
|
737
|
+
const standardEnvKey = field === 'apiKey' ? 'ANTHROPIC_AUTH_TOKEN' : field === 'baseUrl' ? 'ANTHROPIC_BASE_URL' : null;
|
|
738
|
+
if (standardEnvKey) {
|
|
739
|
+
const processValue = process.env[standardEnvKey];
|
|
740
|
+
if (processValue && val !== processValue) {
|
|
741
|
+
block[field] = processValue;
|
|
742
|
+
hasChanges = true;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (hasChanges) {
|
|
750
|
+
const agentConfigPath = path.join(resolvePaths().agentsDir, aid, 'config.json');
|
|
751
|
+
fs.writeFileSync(agentConfigPath, JSON.stringify(agentRaw, null, 2), 'utf-8');
|
|
752
|
+
synced.push(`${aid} (config.json)`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return { data: { synced, count: synced.length } };
|
|
757
|
+
}
|
|
758
|
+
catch (e) {
|
|
759
|
+
return { error: e?.message || String(e), code: 'EXEC_FAILED' };
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/** 从配置中提取所有环境变量引用 */
|
|
763
|
+
function extractEnvVarsFromConfig(baseagents) {
|
|
764
|
+
const envVars = [];
|
|
765
|
+
const seen = new Set();
|
|
766
|
+
for (const type of GATEWAY_TYPES) {
|
|
767
|
+
const block = baseagents[type];
|
|
768
|
+
if (!block)
|
|
769
|
+
continue;
|
|
770
|
+
for (const val of Object.values(block)) {
|
|
771
|
+
if (typeof val === 'string' && val.startsWith(ENV_PREFIX)) {
|
|
772
|
+
const varName = val.slice(ENV_PREFIX.length);
|
|
773
|
+
if (!seen.has(varName)) {
|
|
774
|
+
seen.add(varName);
|
|
775
|
+
const currentValue = process.env[varName] || '';
|
|
776
|
+
envVars.push({ varName, currentValue });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return envVars;
|
|
782
|
+
}
|
|
783
|
+
/** 同步环境变量到 .env 文件(保留现有内容,更新或追加变量) */
|
|
784
|
+
async function syncEnvFile(envPath, envVars) {
|
|
785
|
+
let existingContent = '';
|
|
786
|
+
const existingVars = new Map();
|
|
787
|
+
// 读取现有 .env 文件
|
|
788
|
+
if (fs.existsSync(envPath)) {
|
|
789
|
+
existingContent = fs.readFileSync(envPath, 'utf-8');
|
|
790
|
+
const lines = existingContent.split(/\r?\n/);
|
|
791
|
+
for (const line of lines) {
|
|
792
|
+
const t = line.trim();
|
|
793
|
+
if (!t || t.startsWith('#'))
|
|
794
|
+
continue;
|
|
795
|
+
const eq = t.indexOf('=');
|
|
796
|
+
if (eq <= 0)
|
|
797
|
+
continue;
|
|
798
|
+
const key = t.slice(0, eq).trim();
|
|
799
|
+
existingVars.set(key, line); // 保留原始行(含格式)
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// 构建新内容
|
|
803
|
+
const newLines = [];
|
|
804
|
+
const updatedVars = new Set();
|
|
805
|
+
// 保留现有内容,更新匹配的变量
|
|
806
|
+
if (existingContent) {
|
|
807
|
+
const lines = existingContent.split(/\r?\n/);
|
|
808
|
+
for (const line of lines) {
|
|
809
|
+
const t = line.trim();
|
|
810
|
+
if (!t || t.startsWith('#')) {
|
|
811
|
+
newLines.push(line);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const eq = t.indexOf('=');
|
|
815
|
+
if (eq <= 0) {
|
|
816
|
+
newLines.push(line);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
const key = t.slice(0, eq).trim();
|
|
820
|
+
const envVar = envVars.find(v => v.varName === key);
|
|
821
|
+
if (envVar && envVar.currentValue) {
|
|
822
|
+
// 更新为当前进程环境变量的值
|
|
823
|
+
newLines.push(`${key}=${envVar.currentValue}`);
|
|
824
|
+
updatedVars.add(key);
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
newLines.push(line);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// 追加新的环境变量
|
|
832
|
+
for (const envVar of envVars) {
|
|
833
|
+
if (!updatedVars.has(envVar.varName) && envVar.currentValue) {
|
|
834
|
+
newLines.push(`${envVar.varName}=${envVar.currentValue}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
// 写入文件
|
|
838
|
+
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
839
|
+
fs.writeFileSync(envPath, newLines.join('\n') + '\n', 'utf-8');
|
|
840
|
+
logger.info(`[gateway] 已同步环境变量到: ${envPath}`);
|
|
841
|
+
}
|
|
842
|
+
/** 递归展开 $ENV 引用(仅用于 test 的临时解析,不落盘)。 */
|
|
843
|
+
function expandEnv(obj) {
|
|
844
|
+
if (typeof obj === 'string') {
|
|
845
|
+
if (obj.startsWith(ENV_PREFIX))
|
|
846
|
+
return process.env[obj.slice(ENV_PREFIX.length)] ?? '';
|
|
847
|
+
return obj;
|
|
848
|
+
}
|
|
849
|
+
if (Array.isArray(obj))
|
|
850
|
+
return obj.map(expandEnv);
|
|
851
|
+
if (obj && typeof obj === 'object') {
|
|
852
|
+
const out = {};
|
|
853
|
+
for (const [k, v] of Object.entries(obj))
|
|
854
|
+
out[k] = expandEnv(v);
|
|
855
|
+
return out;
|
|
856
|
+
}
|
|
857
|
+
return obj;
|
|
858
|
+
}
|