evolcore 0.0.20 → 0.0.21

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.
Files changed (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -29,6 +29,7 @@ import { clearRoleStoreCache, isRoleName } from './role-store.js';
29
29
  import { writeRoleDefinition, writeRoleRegistry } from './role-service.js';
30
30
  import { mutateContactBookTransactionSync } from './contact-book-store.js';
31
31
  import { DAEMON_SCHEMA_NAME } from '../product.js';
32
+ import { normalizeDaemonServices } from './daemon-services.js';
32
33
  // AUN gateway discovery belongs to fastaun. Keep the retired gatewayUrl
33
34
  // compatibility handling beside the rest of the configuration read/write
34
35
  // policy so all config callers share one implementation.
@@ -595,11 +596,13 @@ export function read(target, sel, opts = {}) {
595
596
  raw = stripLegacyAunGatewayConfig(raw, file, message => configWarn(message));
596
597
  // schema 版本迁移(read 时若 $schema_version < current)
597
598
  const migrated = migrateIfNeeded(target, raw, file);
598
- const normalized = target === ConfigTarget.Agent
599
- ? normalizeAgentConfigForRead(migrated, sel?.self)
600
- : target === ConfigTarget.Relation
601
- ? foldLegacyProactiveBlock(migrated)
602
- : migrated;
599
+ const normalized = target === ConfigTarget.Process
600
+ ? normalizeProcessConfigCompat(normalizeDaemonServices(migrated))
601
+ : target === ConfigTarget.Agent
602
+ ? normalizeAgentConfigForRead(migrated, sel?.self)
603
+ : target === ConfigTarget.Relation
604
+ ? foldLegacyProactiveBlock(migrated)
605
+ : migrated;
603
606
  // Agent read compatibility may move the legacy top-level `agents` block to
604
607
  // `baseagents`, so canonicalize effort after that structural migration.
605
608
  const canonicalized = normalizeBaseagentEffortCompat(normalized);
@@ -680,12 +683,11 @@ function normalizeAgentConfigForWrite(value) {
680
683
  if (mutable.aid && !isValidAid(mutable.aid)) {
681
684
  throw new ConfigError('VALIDATION_ERROR', `Invalid aid "${mutable.aid}" (must be a valid multi-level domain like mybot.agentid.pub)`);
682
685
  }
683
- // 规范化 projects 字段:只保留 rootPath 和 defaultPath
686
+ // 规范化 projects 字段:只保留唯一公开项目路径 defaultPath
687
+ // defaults.projects.rootPath 已废弃,不能继续成为有效运行时参数。
684
688
  if (mutable.projects && typeof mutable.projects === 'object') {
685
689
  const projects = mutable.projects;
686
690
  const normalized = {};
687
- if (typeof projects.rootPath === 'string')
688
- normalized.rootPath = projects.rootPath;
689
691
  if (typeof projects.defaultPath === 'string')
690
692
  normalized.defaultPath = projects.defaultPath;
691
693
  if (Object.keys(normalized).length > 0) {
@@ -698,19 +700,58 @@ function normalizeAgentConfigForWrite(value) {
698
700
  return mutable;
699
701
  }
700
702
  function normalizeProcessConfigForWrite(value) {
701
- const configuredAidDomain = value.aun?.defaultAidDomain;
703
+ let normalized;
704
+ try {
705
+ normalized = normalizeProcessConfigCompat(normalizeDaemonServices(value));
706
+ }
707
+ catch (error) {
708
+ throw new ConfigError('DAEMON_SERVICES_MIGRATION_REQUIRED', error instanceof Error ? error.message : String(error));
709
+ }
710
+ const configuredAidDomain = normalized.aun?.defaultAidDomain;
702
711
  if (configuredAidDomain === undefined)
703
- return value;
712
+ return normalized;
704
713
  try {
705
714
  return {
706
- ...value,
707
- aun: { ...value.aun, defaultAidDomain: normalizeAidDomain(configuredAidDomain, 'aun.defaultAidDomain') },
715
+ ...normalized,
716
+ aun: { ...normalized.aun, defaultAidDomain: normalizeAidDomain(configuredAidDomain, 'aun.defaultAidDomain') },
708
717
  };
709
718
  }
710
719
  catch (error) {
711
720
  throw new ConfigError('VALIDATION_ERROR', error instanceof Error ? error.message : String(error));
712
721
  }
713
722
  }
723
+ /**
724
+ * Normalize renamed process-level settings while keeping old daemon.json files
725
+ * readable after an upgrade. New values take precedence when both keys exist.
726
+ */
727
+ export function normalizeProcessConfigCompat(value) {
728
+ if (!value || typeof value !== 'object' || Array.isArray(value))
729
+ return value;
730
+ const idleMonitor = value.idleMonitor;
731
+ const debug = value.debug;
732
+ let changed = false;
733
+ let normalized = value;
734
+ if (idleMonitor && typeof idleMonitor === 'object' && !Array.isArray(idleMonitor)) {
735
+ const nextIdleMonitor = { ...idleMonitor };
736
+ if (nextIdleMonitor.apiRetryTimeout === undefined && nextIdleMonitor.retryAttemptTimeout !== undefined) {
737
+ nextIdleMonitor.apiRetryTimeout = nextIdleMonitor.retryAttemptTimeout;
738
+ changed = true;
739
+ }
740
+ if (Object.prototype.hasOwnProperty.call(nextIdleMonitor, 'retryAttemptTimeout')) {
741
+ delete nextIdleMonitor.retryAttemptTimeout;
742
+ changed = true;
743
+ }
744
+ if (changed)
745
+ normalized = { ...normalized, idleMonitor: nextIdleMonitor };
746
+ }
747
+ if (debug && typeof debug === 'object' && !Array.isArray(debug)
748
+ && Object.prototype.hasOwnProperty.call(debug, 'flusherDiag')) {
749
+ const nextDebug = { ...debug };
750
+ delete nextDebug.flusherDiag;
751
+ normalized = { ...normalized, debug: nextDebug };
752
+ }
753
+ return normalized;
754
+ }
714
755
  function normalizeRelationConfigForWrite(value) {
715
756
  const mutable = normalizeBaseagentEffortCompat({ ...value });
716
757
  normalizeShowActivitiesCompat(mutable);
@@ -985,7 +1026,11 @@ function ensureSchemaVersion(value, version) {
985
1026
  }
986
1027
  function validateOrThrow(schema, value, target) {
987
1028
  const ok = schema.validate(value);
988
- const scopeErrors = [...validateProcessOnlyFields(target, value), ...validateProcessAidDomain(target, value)];
1029
+ const scopeErrors = [
1030
+ ...validateProcessOnlyFields(target, value),
1031
+ ...validateProcessAidDomain(target, value),
1032
+ ...validateProcessServiceNames(target, value),
1033
+ ];
989
1034
  if (!ok || scopeErrors.length > 0) {
990
1035
  const errs = [
991
1036
  ...scopeErrors,
@@ -1026,6 +1071,24 @@ function validateProcessAidDomain(target, value) {
1026
1071
  return [`/aun/defaultAidDomain ${error instanceof Error ? error.message : String(error)}`];
1027
1072
  }
1028
1073
  }
1074
+ function validateProcessServiceNames(target, value) {
1075
+ if (target !== ConfigTarget.Process || !value || typeof value !== 'object')
1076
+ return [];
1077
+ const services = value.services;
1078
+ if (!Array.isArray(services))
1079
+ return [];
1080
+ const seen = new Set();
1081
+ const errors = [];
1082
+ services.forEach((service, index) => {
1083
+ if (!service || typeof service !== 'object' || typeof service.name !== 'string')
1084
+ return;
1085
+ if (seen.has(service.name))
1086
+ errors.push(`/services/${index}/name duplicates service name ${JSON.stringify(service.name)}`);
1087
+ else
1088
+ seen.add(service.name);
1089
+ });
1090
+ return errors;
1091
+ }
1029
1092
  /**
1030
1093
  * responseModeParams 桶专项校验。
1031
1094
  *
@@ -1076,8 +1139,28 @@ export function validateConfig(target, value) {
1076
1139
  ? loadSchema('contact-book', 2)
1077
1140
  : loadSchema(TARGET_SCHEMA[target]);
1078
1141
  const withVer = ensureSchemaVersion(value, schema.version);
1079
- const ok = schema.validate(withVer);
1080
- const errors = [...validateProcessOnlyFields(target, withVer), ...validateProcessAidDomain(target, withVer)];
1142
+ let normalized = withVer;
1143
+ try {
1144
+ if (target === ConfigTarget.Process)
1145
+ normalized = normalizeProcessConfigCompat(normalizeDaemonServices(withVer));
1146
+ }
1147
+ catch (error) {
1148
+ return [error instanceof Error ? error.message : String(error)];
1149
+ }
1150
+ const ok = schema.validate(normalized);
1151
+ const errors = [
1152
+ ...validateProcessOnlyFields(target, normalized),
1153
+ ...validateProcessAidDomain(target, normalized),
1154
+ ...validateProcessServiceNames(target, normalized),
1155
+ ];
1156
+ if (target === ConfigTarget.Defaults || target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
1157
+ try {
1158
+ validateResponseModeParams(normalized?.responseModeParams, target);
1159
+ }
1160
+ catch (error) {
1161
+ errors.push(error instanceof ConfigError ? error.message : String(error));
1162
+ }
1163
+ }
1081
1164
  if (ok && errors.length === 0)
1082
1165
  return [];
1083
1166
  return [
@@ -1097,7 +1180,15 @@ export function validateConfigFile(target, sel) {
1097
1180
  return { exists: false, errors: [] };
1098
1181
  // Persisted pre-removal files remain readable: validate the same sanitized
1099
1182
  // view used by runtime reads, while new writes are still rejected by write().
1100
- const sanitized = stripLegacyAunGatewayConfig(value, file, message => configWarn(message));
1183
+ let sanitized;
1184
+ try {
1185
+ sanitized = target === ConfigTarget.Process
1186
+ ? normalizeDaemonServices(stripLegacyAunGatewayConfig(value, file, message => configWarn(message)))
1187
+ : stripLegacyAunGatewayConfig(value, file, message => configWarn(message));
1188
+ }
1189
+ catch (error) {
1190
+ return { exists: true, errors: [error instanceof Error ? error.message : String(error)] };
1191
+ }
1101
1192
  return { exists: true, errors: validateConfig(target, sanitized) };
1102
1193
  }
1103
1194
  function fileCacheAvailable() {
@@ -1159,13 +1250,40 @@ export function resolveAgentConfig(sel, opts = {}) {
1159
1250
  const agentConfig = sel.self ? read(ConfigTarget.Agent, sel, opts) : null;
1160
1251
  const relationConfig = (sel.self && sel.peerKey) ? read(ConfigTarget.Relation, sel, opts) : null;
1161
1252
  const layers = [
1162
- stripStaticOwnerField(read(ConfigTarget.Defaults, undefined, opts)),
1253
+ stripStaticOwnerField(normalizeDefaultsBehaviorCompat(read(ConfigTarget.Defaults, undefined, opts))),
1163
1254
  agentConfig,
1164
1255
  stripRelationRoleData(stripStaticOwnerField(relationConfig)),
1165
1256
  ];
1166
1257
  const merged = mergeLayers(layers, fields);
1167
1258
  return merged;
1168
1259
  }
1260
+ /**
1261
+ * defaults.schema keeps the historical `config` object as the process-wide
1262
+ * response behavior compatibility surface. Make those values participate in
1263
+ * the normal agent/relation merge chain without leaking the compatibility
1264
+ * container itself into EffectiveAgentConfig.
1265
+ *
1266
+ * Explicit canonical fields (if a future schema adds them) always win. A
1267
+ * single default chat mode applies to the three peer categories because this
1268
+ * compatibility field predates the per-category `chatmode` dictionary.
1269
+ */
1270
+ export function normalizeDefaultsBehaviorCompat(defaults) {
1271
+ if (!defaults || !defaults.config || typeof defaults.config !== 'object' || Array.isArray(defaults.config))
1272
+ return defaults;
1273
+ const { config, ...rest } = defaults;
1274
+ const normalized = { ...rest };
1275
+ const chatMode = config.chatMode;
1276
+ if (!normalized.chatmode && (chatMode === 'interactive' || chatMode === 'proactive')) {
1277
+ normalized.chatmode = { private: chatMode, group: chatMode, nothuman: chatMode };
1278
+ }
1279
+ if (normalized.mentionMode === undefined && (config.mentionMode === 'disabled' || config.mentionMode === 'mention-only')) {
1280
+ normalized.mentionMode = config.mentionMode;
1281
+ }
1282
+ if (normalized.show_activities === undefined && ['all', 'text', 'none'].includes(config.show_activities)) {
1283
+ normalized.show_activities = config.show_activities;
1284
+ }
1285
+ return normalized;
1286
+ }
1169
1287
  function stripStaticOwnerField(config) {
1170
1288
  if (!config)
1171
1289
  return config;
@@ -4,7 +4,7 @@ import { isExplicitGroupId } from '../aun/group-identity.js';
4
4
  import { agentConfig } from '../paths.js';
5
5
  import { getContactSnapshot, resolveContactView } from './contact-book.js';
6
6
  import { ContactMutationError } from './contact-book-store.js';
7
- import { mutateContactWithOperation } from './contact-request-service.js';
7
+ import { addContact, mutateContactWithOperation } from './contact-request-service.js';
8
8
  export function resolveContactCommand(argv) {
9
9
  const args = argv[0] === 'contact' ? argv.slice(1) : [...argv];
10
10
  const selfAid = String(argValue(args, '--self') || '').trim();
@@ -39,6 +39,21 @@ export function resolveContactCommand(argv) {
39
39
  },
40
40
  };
41
41
  }
42
+ if (command === 'add') {
43
+ if (!isValidAid(subcommand || '') || isExplicitGroupId(subcommand || '')) {
44
+ return invalid('INVALID_AID', 'primaryId must be a valid individual AID');
45
+ }
46
+ return {
47
+ ok: true,
48
+ command: {
49
+ kind: 'add',
50
+ operationId: 'contact.add',
51
+ selfAid,
52
+ primaryId: subcommand,
53
+ canonicalArgv: canonicalArgv(argv),
54
+ },
55
+ };
56
+ }
42
57
  return invalid('UNKNOWN_COMMAND', `Unknown contact command: ${positional.join(' ')}`);
43
58
  }
44
59
  export async function executeResolvedContactCommand(command, actor = 'local-cli') {
@@ -81,6 +96,22 @@ export async function executeResolvedContactCommand(command, actor = 'local-cli'
81
96
  contactRevision: result.contactRevision,
82
97
  };
83
98
  }
99
+ if (command.kind === 'add') {
100
+ const result = await addContact({
101
+ selfAid: command.selfAid,
102
+ primaryId: command.primaryId,
103
+ actorId: actor,
104
+ });
105
+ return {
106
+ ok: true,
107
+ operation: 'add',
108
+ selfAid: command.selfAid,
109
+ primaryId: command.primaryId,
110
+ status: 'active',
111
+ changed: result.changed,
112
+ contactRevision: result.contactRevision,
113
+ };
114
+ }
84
115
  return { ok: false, code: 'CONTACT_OPERATION_FAILED', error: 'Unsupported contact operation' };
85
116
  }
86
117
  catch (error) {
@@ -38,6 +38,50 @@ export async function mutateContactWithOperation(input) {
38
38
  const { value: _value, ...mutationResult } = result;
39
39
  return mutationResult;
40
40
  }
41
+ /**
42
+ * Add a canonical AUN contact as active through the Contact Book transaction.
43
+ * Existing pending/declined entries are intentionally promoted here, while a
44
+ * blocked entry remains a hard stop and must be explicitly unblocked first.
45
+ */
46
+ export async function addContact(input) {
47
+ const primaryId = String(input.primaryId || '').trim();
48
+ if (!isValidAid(primaryId)) {
49
+ throw new ContactMutationError('INVALID_AID', `Invalid contact AID: ${primaryId}`);
50
+ }
51
+ const at = new Date(input.now ?? Date.now()).toISOString();
52
+ const logFile = agentContactOperations(input.selfAid);
53
+ const result = await mutateContactBookPrepared(input.selfAid, ({ contact, contactRevision }) => {
54
+ if (input.expectedContactRevision !== undefined && input.expectedContactRevision !== contactRevision) {
55
+ throw new ContactMutationError('CONFLICT', 'Contact book changed since it was read', {
56
+ expectedContactRevision: input.expectedContactRevision,
57
+ contactRevision,
58
+ });
59
+ }
60
+ if (contact.contacts[primaryId]?.status === 'blocked') {
61
+ throw new ContactMutationError('CONTACT_BLOCKED', `Contact is blocked: ${primaryId}`);
62
+ }
63
+ const mutations = [
64
+ { type: 'set-status', primaryId, status: 'active' },
65
+ ];
66
+ if (input.displayName !== undefined) {
67
+ mutations.push({ type: 'set-display-name', primaryId, displayName: input.displayName });
68
+ }
69
+ return {
70
+ mutation: mutations.length === 1 ? mutations[0] : { type: 'batch', mutations },
71
+ actor: input.actorId,
72
+ value: undefined,
73
+ beforeCommit: ({ contactRevision: nextRevision }) => operationLogChange(logFile, [operationRecord({
74
+ at,
75
+ op: 'manual-add',
76
+ primaryId,
77
+ actorId: input.actorId,
78
+ contactRevision: nextRevision,
79
+ })]),
80
+ };
81
+ });
82
+ const { value: _value, ...mutationResult } = result;
83
+ return mutationResult;
84
+ }
41
85
  export async function submitContactRequest(input) {
42
86
  const applicantAid = String(input.applicantAid || '').trim();
43
87
  const owners = listAgentOwners(input.selfAid);
@@ -0,0 +1,186 @@
1
+ const SERVICE_TYPES = new Set(['http', 'websocket', 'ssh']);
2
+ const SERVICE_VISIBILITIES = new Set(['public', 'private', 'aun-auth']);
3
+ export const DEFAULT_ECWEB_PORT = 42705;
4
+ function isRecord(value) {
5
+ return !!value && typeof value === 'object' && !Array.isArray(value);
6
+ }
7
+ /**
8
+ * Convert the former `ecweb` + `serviceProxy` process configuration into the
9
+ * service-instance model. The function is intentionally idempotent and does
10
+ * not mutate its input.
11
+ */
12
+ export function normalizeDaemonServices(raw, warn) {
13
+ if (!isRecord(raw))
14
+ return raw;
15
+ const next = { ...raw };
16
+ const hasLegacyEcweb = Object.prototype.hasOwnProperty.call(raw, 'ecweb');
17
+ const hasLegacyProxy = Object.prototype.hasOwnProperty.call(raw, 'serviceProxy');
18
+ const hasCanonicalServices = Object.prototype.hasOwnProperty.call(raw, 'services');
19
+ const services = [];
20
+ if (hasCanonicalServices) {
21
+ if (!Array.isArray(raw.services)) {
22
+ throw new Error('daemon.json.services must be an array');
23
+ }
24
+ for (const [index, value] of raw.services.entries()) {
25
+ if (!isRecord(value)) {
26
+ throw new Error(`daemon.json.services[${index}] must be an object`);
27
+ }
28
+ if (typeof value.name !== 'string' || value.name.trim() === '') {
29
+ throw new Error(`daemon.json.services[${index}].name must be a non-empty string`);
30
+ }
31
+ // Preserve the canonical object for schema validation. In particular,
32
+ // do not trim/drop fields here, otherwise malformed new-format config
33
+ // could be silently rewritten into a different valid-looking shape.
34
+ services.push({ ...value });
35
+ }
36
+ }
37
+ if (hasLegacyEcweb && !isRecord(raw.ecweb)) {
38
+ throw new Error('daemon.json.ecweb must be an object before it can be migrated');
39
+ }
40
+ if (hasLegacyProxy && !isRecord(raw.serviceProxy)) {
41
+ throw new Error('daemon.json.serviceProxy must be an object before it can be migrated');
42
+ }
43
+ const legacyEcweb = hasLegacyEcweb ? raw.ecweb : undefined;
44
+ const legacyProxy = hasLegacyProxy ? raw.serviceProxy : undefined;
45
+ if (legacyEcweb?.enabled !== undefined && typeof legacyEcweb.enabled !== 'boolean') {
46
+ throw new Error('daemon.json.ecweb.enabled must be a boolean before it can be migrated');
47
+ }
48
+ if (legacyEcweb?.port !== undefined && typeof legacyEcweb.port !== 'number') {
49
+ throw new Error('daemon.json.ecweb.port must be a number before it can be migrated');
50
+ }
51
+ if (legacyProxy?.enabled !== undefined && typeof legacyProxy.enabled !== 'boolean') {
52
+ throw new Error('daemon.json.serviceProxy.enabled must be a boolean before it can be migrated');
53
+ }
54
+ const legacyProxyServices = [];
55
+ if (legacyProxy?.services !== undefined) {
56
+ if (!Array.isArray(legacyProxy.services)) {
57
+ throw new Error('daemon.json.serviceProxy.services must be an array before it can be migrated');
58
+ }
59
+ for (const [index, value] of legacyProxy.services.entries()) {
60
+ if (!isRecord(value)) {
61
+ throw new Error(`daemon.json.serviceProxy.services[${index}] cannot be represented by source="instance"`);
62
+ }
63
+ if (value.enabled !== undefined && typeof value.enabled !== 'boolean') {
64
+ throw new Error(`daemon.json.serviceProxy.services[${index}].enabled must be a boolean before it can be migrated`);
65
+ }
66
+ if (value.visibility !== undefined && typeof value.visibility !== 'string') {
67
+ throw new Error(`daemon.json.serviceProxy.services[${index}].visibility must be a string before it can be migrated`);
68
+ }
69
+ legacyProxyServices.push(value);
70
+ }
71
+ }
72
+ const legacyEcwebProxy = legacyProxyServices.find((value) => value.name === 'ecweb');
73
+ const ecwebIndex = services.findIndex((service) => service.name === 'ecweb');
74
+ // Convert the former flat serviceProxy entries into canonical services[].
75
+ // Keep every service rather than silently dropping non-ecweb entries.
76
+ for (const legacy of legacyProxyServices) {
77
+ const name = typeof legacy.name === 'string' ? legacy.name : '';
78
+ if (!name || name === 'ecweb')
79
+ continue;
80
+ if (services.some((service) => service.name === name))
81
+ continue;
82
+ const source = legacy.source === 'ecweb'
83
+ ? 'instance'
84
+ : legacy.source === 'instance'
85
+ ? 'instance'
86
+ : typeof legacy.endpoint === 'string' && legacy.endpoint.trim()
87
+ ? 'static'
88
+ : 'static';
89
+ const proxy = {
90
+ enabled: legacyProxy?.enabled === true && legacy.enabled !== false,
91
+ source,
92
+ ...(typeof legacy.endpoint === 'string' ? { endpoint: legacy.endpoint } : {}),
93
+ ...(typeof legacy.serviceType === 'string' && SERVICE_TYPES.has(legacy.serviceType)
94
+ ? { serviceType: legacy.serviceType } : {}),
95
+ ...(typeof legacy.visibility === 'string' ? { visibility: legacy.visibility } : {}),
96
+ ...(isRecord(legacy.metadata) ? { metadata: { ...legacy.metadata } } : {}),
97
+ };
98
+ services.push({ name, enabled: true, proxy });
99
+ }
100
+ if (legacyEcweb || legacyEcwebProxy) {
101
+ const service = ecwebIndex >= 0
102
+ ? services[ecwebIndex]
103
+ : {
104
+ name: 'ecweb',
105
+ // The old ECWeb lifecycle switch and Service Proxy switch were
106
+ // independent. Preserve that distinction during migration: a
107
+ // proxy-only config must not start managing the local process.
108
+ enabled: legacyEcweb?.enabled === true,
109
+ };
110
+ if (legacyEcweb) {
111
+ if (service.enabled === undefined && typeof legacyEcweb.enabled === 'boolean')
112
+ service.enabled = legacyEcweb.enabled;
113
+ if (service.port === undefined && typeof legacyEcweb.port === 'number')
114
+ service.port = legacyEcweb.port;
115
+ }
116
+ if (service.proxy !== undefined && !isRecord(service.proxy)) {
117
+ throw new Error('daemon.json.services[ecweb].proxy must be an object before legacy proxy settings can be migrated');
118
+ }
119
+ const proxy = { ...(service.proxy ?? {}) };
120
+ if (legacyEcwebProxy) {
121
+ if (proxy.enabled === undefined) {
122
+ proxy.enabled = legacyProxy?.enabled === true && legacyEcwebProxy.enabled !== false;
123
+ }
124
+ if (proxy.source === undefined) {
125
+ proxy.source = legacyEcwebProxy.source === 'static'
126
+ || (typeof legacyEcwebProxy.endpoint === 'string' && legacyEcwebProxy.endpoint.trim())
127
+ ? 'static'
128
+ : 'instance';
129
+ }
130
+ if (proxy.endpoint === undefined && typeof legacyEcwebProxy.endpoint === 'string')
131
+ proxy.endpoint = legacyEcwebProxy.endpoint;
132
+ if (proxy.serviceType === undefined && typeof legacyEcwebProxy.serviceType === 'string'
133
+ && SERVICE_TYPES.has(legacyEcwebProxy.serviceType)) {
134
+ proxy.serviceType = legacyEcwebProxy.serviceType;
135
+ }
136
+ if (proxy.visibility === undefined && typeof legacyEcwebProxy.visibility === 'string') {
137
+ proxy.visibility = legacyEcwebProxy.visibility;
138
+ }
139
+ if (proxy.metadata === undefined && isRecord(legacyEcwebProxy.metadata)) {
140
+ proxy.metadata = { ...legacyEcwebProxy.metadata };
141
+ }
142
+ if (proxy.enabled === true && proxy.visibility === undefined)
143
+ proxy.visibility = 'private';
144
+ }
145
+ if (Object.keys(proxy).length > 0)
146
+ service.proxy = proxy;
147
+ if (ecwebIndex < 0)
148
+ services.push(service);
149
+ }
150
+ if (hasCanonicalServices || services.length > 0)
151
+ next.services = services;
152
+ if (hasLegacyEcweb || hasLegacyProxy) {
153
+ warn?.('[config] daemon.json: ecweb/serviceProxy 已归一化为 services[]');
154
+ delete next.ecweb;
155
+ delete next.serviceProxy;
156
+ }
157
+ return next;
158
+ }
159
+ export function findService(services, name) {
160
+ return (services ?? []).find((service) => service.name === name);
161
+ }
162
+ export function ecwebService(services) {
163
+ return findService(services, 'ecweb');
164
+ }
165
+ export function upsertService(services, patch) {
166
+ const current = services ?? [];
167
+ const index = current.findIndex((service) => service.name === patch.name);
168
+ if (index < 0)
169
+ return [...current, patch];
170
+ const existing = current[index];
171
+ const replacement = {
172
+ ...existing,
173
+ ...patch,
174
+ ...(existing.proxy || patch.proxy
175
+ ? { proxy: { ...(existing.proxy ?? {}), ...(patch.proxy ?? {}) } }
176
+ : {}),
177
+ };
178
+ return current.map((service, serviceIndex) => serviceIndex === index ? replacement : service);
179
+ }
180
+ export function setEcwebEnabled(services, enabled, port = ecwebService(services)?.port ?? DEFAULT_ECWEB_PORT) {
181
+ return upsertService(services, {
182
+ name: 'ecweb',
183
+ enabled,
184
+ port,
185
+ });
186
+ }
@@ -17,6 +17,7 @@ import { resolvePaths } from '../paths.js';
17
17
  import { saveDefaultsSafe, saveAgent } from '../config-store.js';
18
18
  import { resolveAnthropicConfig, resolveEcagentConfig, resolveOpenaiConfig } from '../agents/baseagent.js';
19
19
  import { buildModelRequestHeaders } from '../agents/request-identity.js';
20
+ import { appendV1RequestPath } from '../agents/request-identity.js';
20
21
  import { resolvePriceRow } from '../stats/billing.js';
21
22
  import { ipcQuery } from '../ipc.js';
22
23
  import { logger } from '../utils/logger.js';
@@ -541,6 +542,7 @@ function resolveGatewayCreds(scope, type) {
541
542
  baseUrl: r.baseUrl,
542
543
  apiKey: r.apiKey,
543
544
  headers: buildModelRequestHeaders({ baseagent: 'codex', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
545
+ queryParams: r.queryParams,
544
546
  };
545
547
  }
546
548
  else if (type === 'claude') {
@@ -549,6 +551,7 @@ function resolveGatewayCreds(scope, type) {
549
551
  baseUrl: r.baseUrl,
550
552
  apiKey: r.apiKey,
551
553
  headers: buildModelRequestHeaders({ baseagent: 'claude', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
554
+ queryParams: r.queryParams,
552
555
  };
553
556
  }
554
557
  else if (type === 'ecagent') {
@@ -557,6 +560,7 @@ function resolveGatewayCreds(scope, type) {
557
560
  baseUrl: r.baseUrl,
558
561
  apiKey: r.apiKey,
559
562
  headers: buildModelRequestHeaders({ baseagent: 'ecagent', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
563
+ queryParams: r.queryParams,
560
564
  };
561
565
  }
562
566
  return { error: 'gemini 暂不支持 HTTP 探测(CLI 后端)', code: 'NOT_SUPPORTED' };
@@ -574,20 +578,19 @@ export async function gatewayTest(args) {
574
578
  const creds = resolveGatewayCreds(scope, type);
575
579
  if ('error' in creds)
576
580
  return creds;
577
- const { baseUrl, apiKey, headers } = creds;
581
+ const { baseUrl, apiKey, headers, queryParams } = creds;
578
582
  if (!baseUrl)
579
583
  return { error: '未配置 baseUrl(或为官方占位地址)', code: 'INVALID_ARGS' };
580
584
  const start = Date.now();
585
+ let timer;
581
586
  try {
582
587
  const controller = new AbortController();
583
- const timer = setTimeout(() => controller.abort(), 5000);
584
- const base = baseUrl.replace(/\/+$/, '');
585
- const modelsUrl = base.endsWith('/v1') ? `${base}/models` : `${base}/v1/models`;
588
+ timer = setTimeout(() => controller.abort(), 5000);
589
+ const modelsUrl = appendV1RequestPath(baseUrl, 'models', queryParams);
586
590
  const resp = await fetch(modelsUrl, {
587
591
  headers: { ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), ...headers },
588
592
  signal: controller.signal,
589
593
  });
590
- clearTimeout(timer);
591
594
  const latency = Date.now() - start;
592
595
  if (!resp.ok)
593
596
  return { data: { ok: false, latency, error: `HTTP ${resp.status}` } };
@@ -600,6 +603,10 @@ export async function gatewayTest(args) {
600
603
  catch (e) {
601
604
  return { data: { ok: false, latency: Date.now() - start, error: e?.message || String(e) } };
602
605
  }
606
+ finally {
607
+ if (timer)
608
+ clearTimeout(timer);
609
+ }
603
610
  }
604
611
  /** 从 model-prices.jsonl 的 PriceRecord 提取展示用价格四元组。 */
605
612
  function priceRowToQuad(row) {
@@ -632,19 +639,19 @@ export async function gatewayModels(args) {
632
639
  const creds = resolveGatewayCreds(scope, type);
633
640
  if ('error' in creds)
634
641
  return creds;
635
- const { baseUrl, apiKey, headers } = creds;
642
+ const { baseUrl, apiKey, headers, queryParams } = creds;
636
643
  if (!baseUrl)
637
644
  return { error: '未配置 baseUrl(或为官方占位地址)', code: 'INVALID_ARGS' };
638
645
  let arr = [];
639
646
  let usdToCny = 7; // 默认汇率
647
+ let timer;
640
648
  try {
641
649
  const controller = new AbortController();
642
- const timer = setTimeout(() => controller.abort(), 8000);
643
- const resp = await fetch(`${baseUrl.replace(/\/+$/, '')}/v1/models`, {
650
+ timer = setTimeout(() => controller.abort(), 8000);
651
+ const resp = await fetch(appendV1RequestPath(baseUrl, 'models', queryParams), {
644
652
  headers: { ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), ...headers },
645
653
  signal: controller.signal,
646
654
  });
647
- clearTimeout(timer);
648
655
  if (!resp.ok)
649
656
  return { error: `HTTP ${resp.status}`, code: 'EXEC_FAILED' };
650
657
  const json = await resp.json().catch(() => null);
@@ -657,6 +664,10 @@ export async function gatewayModels(args) {
657
664
  catch (e) {
658
665
  return { error: `请求失败:${e?.message || e}`, code: 'EXEC_FAILED' };
659
666
  }
667
+ finally {
668
+ if (timer)
669
+ clearTimeout(timer);
670
+ }
660
671
  const evolcoreHome = resolvePaths().root;
661
672
  const now = Date.now();
662
673
  const models = arr.map((m) => {