evolcore 0.0.27 → 0.0.28
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 +26 -15
- package/dist/agents/baseagent.js +10 -5
- package/dist/agents/codex/app-server-client.js +7 -0
- package/dist/agents/codex/catalog.js +7 -1
- package/dist/agents/codex-runner.js +48 -11
- package/dist/aun/menu/client.js +1 -0
- package/dist/channels/aun.js +23 -7
- package/dist/cli/daemon-commands.js +77 -140
- package/dist/cli/diag.js +1 -1
- package/dist/cli/menu-client.js +4 -0
- package/dist/cli/restart-monitor.js +65 -17
- package/dist/core/command/command-handler.js +29 -1
- package/dist/core/command/diag-service.js +292 -27
- package/dist/core/command/menu-handler.js +16 -19
- package/dist/core/command/menu-protocol.js +3 -0
- package/dist/core/command/slash-handler.js +24 -15
- package/dist/core/message/im-renderer.js +21 -4
- package/dist/core/message/message-bridge.js +47 -3
- package/dist/core/message/response-engine.js +29 -10
- package/dist/core/permission/ec-command-parser.js +15 -7
- package/dist/core/permission/tool-policy.js +1 -1
- package/dist/core/session/session-manager.js +4 -0
- package/dist/index.js +14 -153
- package/dist/product.js +1 -0
- package/dist/utils/atomic-write.js +98 -0
- package/dist/utils/cross-platform.js +87 -16
- package/dist/utils/npm-ops.js +119 -4
- package/dist/utils/package-version-info.js +244 -0
- package/dist/utils/windows-shell-trust.js +37 -0
- package/kits/docs/channels/aun.md +8 -0
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { promisify } from 'util';
|
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import { getProcessStartTime, parseCimDate } from './process-introspect.js';
|
|
8
8
|
import { decodeWindowsOutput } from './windows-output.js';
|
|
9
|
+
import { resolveTrustedWindowsCmdPath } from './windows-shell-trust.js';
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
const CLI_PROBE_FAILURE_BACKOFF_MS = 5_000;
|
|
11
12
|
const CLI_PROBE_PACKAGES = {
|
|
@@ -17,9 +18,70 @@ const cliPathCache = new Map();
|
|
|
17
18
|
const cliInstallationCache = new Map();
|
|
18
19
|
const cliProbeCache = new Map();
|
|
19
20
|
export const isWindows = process.platform === 'win32';
|
|
21
|
+
// Keep this in sync with the Windows command-line metacharacters. The whole
|
|
22
|
+
// `/c` expression is passed as one verbatim argument so paths with spaces and
|
|
23
|
+
// shim-forwarded values such as `%`, `!`, `&`, and quotes stay data.
|
|
24
|
+
const WINDOWS_CMD_META_RE = /([()[\]%!^"`<>&|;, *?])/g;
|
|
25
|
+
function escapeWindowsCmdCommand(value) {
|
|
26
|
+
return value.replace(WINDOWS_CMD_META_RE, '^$1');
|
|
27
|
+
}
|
|
28
|
+
function escapeWindowsCmdArgument(value) {
|
|
29
|
+
let escaped = String(value);
|
|
30
|
+
escaped = escaped.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"');
|
|
31
|
+
escaped = escaped.replace(/(?=(\\+?)?)\1$/g, '$1$1');
|
|
32
|
+
escaped = `"${escaped}"`;
|
|
33
|
+
return escaped.replace(WINDOWS_CMD_META_RE, '^$1');
|
|
34
|
+
}
|
|
35
|
+
/** Build one argv-based CLI launch without consulting the parent terminal. */
|
|
36
|
+
export function buildCliLaunchCommand(executable, args, options = {}) {
|
|
37
|
+
const windows = options.isWindows ?? isWindows;
|
|
38
|
+
const extension = windows ? path.win32.extname(executable).toLowerCase() : '';
|
|
39
|
+
const shim = extension === '.cmd' || extension === '.bat';
|
|
40
|
+
if (!shim) {
|
|
41
|
+
return {
|
|
42
|
+
command: executable,
|
|
43
|
+
args,
|
|
44
|
+
resolvedPath: executable,
|
|
45
|
+
launcherKind: 'native-exe',
|
|
46
|
+
launcherPath: executable,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const resolveWindowsCmd = options.resolveWindowsCmd ?? resolveTrustedWindowsCmdPath;
|
|
50
|
+
const launcherPath = resolveWindowsCmd(options.comSpec);
|
|
51
|
+
if (!launcherPath) {
|
|
52
|
+
throw Object.assign(new Error('未找到受信任的系统 cmd.exe,已拒绝启动 Windows CLI shim'), {
|
|
53
|
+
code: 'EACCES',
|
|
54
|
+
path: executable,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const shellCommand = [
|
|
58
|
+
escapeWindowsCmdCommand(executable),
|
|
59
|
+
...args.map(escapeWindowsCmdArgument),
|
|
60
|
+
].join(' ');
|
|
61
|
+
return {
|
|
62
|
+
command: launcherPath,
|
|
63
|
+
args: ['/d', '/s', '/v:off', '/c', `"${shellCommand}"`],
|
|
64
|
+
resolvedPath: executable,
|
|
65
|
+
launcherKind: extension === '.cmd' ? 'cmd-shim' : 'bat-shim',
|
|
66
|
+
launcherPath,
|
|
67
|
+
windowsVerbatimArguments: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
20
70
|
/** Resolve a CLI executable without spawning where/which. */
|
|
21
71
|
export function resolveProbePath(command) {
|
|
22
|
-
const
|
|
72
|
+
const extensions = isWindows
|
|
73
|
+
? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(ext => /\.(com|exe|bat|cmd)$/i.test(ext))
|
|
74
|
+
: [''];
|
|
75
|
+
return resolveProbePathWithExtensions(command, extensions, 'executable');
|
|
76
|
+
}
|
|
77
|
+
/** Diagnose an incomplete npm installation without ever executing its PS1 shim. */
|
|
78
|
+
export function resolvePowerShellShimPath(command) {
|
|
79
|
+
if (!isWindows)
|
|
80
|
+
return null;
|
|
81
|
+
return resolveProbePathWithExtensions(command, ['.ps1'], 'powershell-shim');
|
|
82
|
+
}
|
|
83
|
+
function resolveProbePathWithExtensions(command, extensions, cacheKind) {
|
|
84
|
+
const pathKey = `${cacheKind}\0${command}\0${process.cwd()}\0${process.env.PATH ?? ''}\0${extensions.join(';')}`;
|
|
23
85
|
const cachedPath = cliPathCache.get(pathKey);
|
|
24
86
|
if (cachedPath && fs.existsSync(cachedPath))
|
|
25
87
|
return cachedPath;
|
|
@@ -27,9 +89,6 @@ export function resolveProbePath(command) {
|
|
|
27
89
|
const directories = (process.env.PATH ?? '').split(path.delimiter);
|
|
28
90
|
if (isWindows)
|
|
29
91
|
directories.unshift(process.cwd());
|
|
30
|
-
const extensions = isWindows
|
|
31
|
-
? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(ext => /\.(com|exe|bat|cmd)$/i.test(ext))
|
|
32
|
-
: [''];
|
|
33
92
|
for (const directory of directories) {
|
|
34
93
|
for (const extension of extensions) {
|
|
35
94
|
const candidate = path.resolve(directory.replace(/^"|"$/g, ''), command + extension);
|
|
@@ -98,20 +157,25 @@ export function invalidateCliProbes() {
|
|
|
98
157
|
function cliProbeTimeouts(command, options) {
|
|
99
158
|
return options.timeouts?.length ? options.timeouts : command === 'codex' ? [3_000, 15_000] : [3_000];
|
|
100
159
|
}
|
|
101
|
-
function prepareCliProbe(command, args) {
|
|
102
|
-
const executable = resolveProbePath(command);
|
|
160
|
+
function prepareCliProbe(command, args, options = {}) {
|
|
161
|
+
const executable = options.executable ?? resolveProbePath(command);
|
|
103
162
|
if (!executable)
|
|
104
163
|
throw Object.assign(new Error(`${command} CLI not found`), { code: 'ENOENT' });
|
|
105
|
-
const
|
|
164
|
+
const launchCommand = buildCliLaunchCommand(executable, args);
|
|
165
|
+
const launch = launchCommand;
|
|
166
|
+
const identity = `${probeInstallationIdentity(command, executable)}|${cliFileIdentity(launch.launcherPath)}`;
|
|
106
167
|
const key = JSON.stringify([command, args]);
|
|
107
168
|
let entry = cliProbeCache.get(key);
|
|
108
169
|
if (!entry || entry.identity !== identity || (entry.retryAt !== undefined && Date.now() >= entry.retryAt)) {
|
|
109
170
|
entry = { identity };
|
|
110
171
|
cliProbeCache.set(key, entry);
|
|
111
172
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
173
|
+
return {
|
|
174
|
+
entry,
|
|
175
|
+
command: launchCommand.command,
|
|
176
|
+
args: launchCommand.args,
|
|
177
|
+
launch,
|
|
178
|
+
};
|
|
115
179
|
}
|
|
116
180
|
function failCliProbe(entry, error) {
|
|
117
181
|
entry.error = error;
|
|
@@ -119,7 +183,7 @@ function failCliProbe(entry, error) {
|
|
|
119
183
|
throw error;
|
|
120
184
|
}
|
|
121
185
|
export function probeCliSync(command, args, options = {}) {
|
|
122
|
-
const request = prepareCliProbe(command, args);
|
|
186
|
+
const request = prepareCliProbe(command, args, options);
|
|
123
187
|
if (request.entry.output !== undefined)
|
|
124
188
|
return request.entry.output;
|
|
125
189
|
if (request.entry.error !== undefined)
|
|
@@ -127,10 +191,14 @@ export function probeCliSync(command, args, options = {}) {
|
|
|
127
191
|
for (const [index, timeout] of cliProbeTimeouts(command, options).entries()) {
|
|
128
192
|
const startedAt = Date.now();
|
|
129
193
|
try {
|
|
130
|
-
return request.entry.output = execFileSync(request.command, request.args, {
|
|
194
|
+
return request.entry.output = execFileSync(request.command, request.args, {
|
|
195
|
+
encoding: 'utf8', timeout, windowsHide: true,
|
|
196
|
+
windowsVerbatimArguments: request.launch.windowsVerbatimArguments,
|
|
197
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
198
|
+
});
|
|
131
199
|
}
|
|
132
200
|
catch (error) {
|
|
133
|
-
options.onFailure?.(args, startedAt, error, timeout);
|
|
201
|
+
options.onFailure?.(args, startedAt, error, timeout, request.launch);
|
|
134
202
|
if (error.code !== 'ETIMEDOUT' || index === cliProbeTimeouts(command, options).length - 1)
|
|
135
203
|
return failCliProbe(request.entry, error);
|
|
136
204
|
}
|
|
@@ -138,7 +206,7 @@ export function probeCliSync(command, args, options = {}) {
|
|
|
138
206
|
throw new Error('CLI probe exhausted');
|
|
139
207
|
}
|
|
140
208
|
export async function probeCli(command, args, options = {}) {
|
|
141
|
-
const request = prepareCliProbe(command, args);
|
|
209
|
+
const request = prepareCliProbe(command, args, options);
|
|
142
210
|
if (request.entry.output !== undefined)
|
|
143
211
|
return request.entry.output;
|
|
144
212
|
if (request.entry.error !== undefined)
|
|
@@ -151,7 +219,10 @@ export async function probeCli(command, args, options = {}) {
|
|
|
151
219
|
const startedAt = Date.now();
|
|
152
220
|
try {
|
|
153
221
|
const output = await new Promise((resolve, reject) => {
|
|
154
|
-
const child = execFile(request.command, request.args, {
|
|
222
|
+
const child = execFile(request.command, request.args, {
|
|
223
|
+
encoding: 'utf8', timeout, windowsHide: true,
|
|
224
|
+
windowsVerbatimArguments: request.launch.windowsVerbatimArguments,
|
|
225
|
+
}, (error, stdout, stderr) => {
|
|
155
226
|
if (error) {
|
|
156
227
|
if (error.killed && error.signal === 'SIGTERM' && error.code == null)
|
|
157
228
|
error.code = 'ETIMEDOUT';
|
|
@@ -166,7 +237,7 @@ export async function probeCli(command, args, options = {}) {
|
|
|
166
237
|
return request.entry.output = output;
|
|
167
238
|
}
|
|
168
239
|
catch (error) {
|
|
169
|
-
options.onFailure?.(args, startedAt, error, timeout);
|
|
240
|
+
options.onFailure?.(args, startedAt, error, timeout, request.launch);
|
|
170
241
|
if (error.code !== 'ETIMEDOUT' || index === timeouts.length - 1)
|
|
171
242
|
return failCliProbe(request.entry, error);
|
|
172
243
|
}
|
package/dist/utils/npm-ops.js
CHANGED
|
@@ -8,11 +8,26 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
11
12
|
import { execFile, execFileSync } from 'child_process';
|
|
12
13
|
import { promisify } from 'util';
|
|
13
14
|
import { getPackageRoot } from '../paths.js';
|
|
15
|
+
import { DEFAULT_RUNTIME_HOME_DIRNAME, MAIN_PACKAGE_NAME, RUNTIME_HOME_ENV, TARGET_PACKAGE_ROOT_ENV, WEB_PACKAGE_NAME, } from '../product.js';
|
|
14
16
|
import { isWindows } from './cross-platform.js';
|
|
17
|
+
import { withFileLock } from './atomic-write.js';
|
|
15
18
|
const execFileAsync = promisify(execFile);
|
|
19
|
+
function globalInstallLockPath() {
|
|
20
|
+
const home = process.env[RUNTIME_HOME_ENV] || path.join(os.homedir(), DEFAULT_RUNTIME_HOME_DIRNAME);
|
|
21
|
+
return path.join(home, 'data', 'npm-global-install.lock');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The restart monitor may execute from a staged copy of the package while
|
|
25
|
+
* upgrading the real installation. Keep code loading rooted at getPackageRoot
|
|
26
|
+
* but resolve install layout and local versions from the package being served.
|
|
27
|
+
*/
|
|
28
|
+
function targetPackageRoot() {
|
|
29
|
+
return process.env[TARGET_PACKAGE_ROOT_ENV] || getPackageRoot();
|
|
30
|
+
}
|
|
16
31
|
/** The preferred public registry for update checks. */
|
|
17
32
|
export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
18
33
|
function normalizeRegistry(registry = DEFAULT_NPM_REGISTRY) {
|
|
@@ -55,7 +70,7 @@ function resolveNpmInvocation() {
|
|
|
55
70
|
* default. A command failure is handled separately by using the public source
|
|
56
71
|
* directly, so a broken npm config cannot leave the upgrade without a retry.
|
|
57
72
|
*/
|
|
58
|
-
async function resolveUpgradeRegistry(signal) {
|
|
73
|
+
export async function resolveUpgradeRegistry(signal) {
|
|
59
74
|
const npm = resolveNpmInvocation();
|
|
60
75
|
try {
|
|
61
76
|
const result = await execFileAsync(npm.command, [...npm.argsPrefix, 'config', 'get', 'registry'], {
|
|
@@ -115,7 +130,7 @@ async function withGlobalInstallLock(action) {
|
|
|
115
130
|
globalInstallTail = new Promise(resolve => { release = resolve; });
|
|
116
131
|
await previous;
|
|
117
132
|
try {
|
|
118
|
-
await
|
|
133
|
+
await withFileLock(globalInstallLockPath(), action, 5 * 60_000);
|
|
119
134
|
}
|
|
120
135
|
finally {
|
|
121
136
|
release();
|
|
@@ -191,6 +206,59 @@ export async function requireOptional(pkg, autoInstall = true) {
|
|
|
191
206
|
return await import(pkg);
|
|
192
207
|
}
|
|
193
208
|
}
|
|
209
|
+
const PACKAGE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
210
|
+
export function isPackageVersion(value) {
|
|
211
|
+
if (typeof value !== 'string' || value.length > 256)
|
|
212
|
+
return false;
|
|
213
|
+
const match = PACKAGE_VERSION.exec(value);
|
|
214
|
+
return !!match && (!match[4] || match[4].split('.').every(id => !/^\d+$/.test(id) || id === '0' || !id.startsWith('0')));
|
|
215
|
+
}
|
|
216
|
+
export function normalizeVersionRegistry(value) {
|
|
217
|
+
if (typeof value !== 'string')
|
|
218
|
+
return null;
|
|
219
|
+
try {
|
|
220
|
+
const url = new URL(value);
|
|
221
|
+
if (!['https:', 'http:'].includes(url.protocol) || !url.hostname
|
|
222
|
+
|| url.username || url.password || url.search || url.hash)
|
|
223
|
+
return null;
|
|
224
|
+
return url.href.replace(/\/+$/, '');
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export function comparePackageVersions(a, b) {
|
|
231
|
+
if (!isPackageVersion(a) || !isPackageVersion(b))
|
|
232
|
+
throw new Error('Invalid package version');
|
|
233
|
+
const left = PACKAGE_VERSION.exec(a);
|
|
234
|
+
const right = PACKAGE_VERSION.exec(b);
|
|
235
|
+
for (let i = 1; i <= 3; i++) {
|
|
236
|
+
if (BigInt(left[i]) < BigInt(right[i]))
|
|
237
|
+
return -1;
|
|
238
|
+
if (BigInt(left[i]) > BigInt(right[i]))
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
if (!left[4] || !right[4])
|
|
242
|
+
return left[4] === right[4] ? 0 : left[4] ? -1 : 1;
|
|
243
|
+
const la = left[4].split('.');
|
|
244
|
+
const ra = right[4].split('.');
|
|
245
|
+
for (let i = 0; i < Math.max(la.length, ra.length); i++) {
|
|
246
|
+
if (la[i] === ra[i])
|
|
247
|
+
continue;
|
|
248
|
+
if (la[i] === undefined)
|
|
249
|
+
return -1;
|
|
250
|
+
if (ra[i] === undefined)
|
|
251
|
+
return 1;
|
|
252
|
+
const ln = /^\d+$/.test(la[i]);
|
|
253
|
+
const rn = /^\d+$/.test(ra[i]);
|
|
254
|
+
if (ln && rn)
|
|
255
|
+
return BigInt(la[i]) < BigInt(ra[i]) ? -1 : 1;
|
|
256
|
+
if (ln !== rn)
|
|
257
|
+
return ln ? -1 : 1;
|
|
258
|
+
return la[i] < ra[i] ? -1 : 1;
|
|
259
|
+
}
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
194
262
|
/**
|
|
195
263
|
* 比较两个 semver 版本号 (a.b.c 格式)
|
|
196
264
|
* 返回 -1 (a < b), 0 (a == b), 1 (a > b)
|
|
@@ -216,12 +284,12 @@ export function compareVersions(a, b) {
|
|
|
216
284
|
* 而 npm link 指向项目源码目录,其父目录不是 node_modules。
|
|
217
285
|
*/
|
|
218
286
|
export function isLinkedInstall() {
|
|
219
|
-
const pkgRoot =
|
|
287
|
+
const pkgRoot = targetPackageRoot();
|
|
220
288
|
return path.basename(path.dirname(pkgRoot)) !== 'node_modules';
|
|
221
289
|
}
|
|
222
290
|
/** 获取本地 package.json 中的版本号 */
|
|
223
291
|
export function getLocalVersion() {
|
|
224
|
-
const pkgPath = path.join(
|
|
292
|
+
const pkgPath = path.join(targetPackageRoot(), 'package.json');
|
|
225
293
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
226
294
|
return pkg.version;
|
|
227
295
|
}
|
|
@@ -276,6 +344,53 @@ async function fetchLatestVersion(pkg = 'evolcore', timeoutMs = 10000, signal, r
|
|
|
276
344
|
signal?.removeEventListener('abort', abort);
|
|
277
345
|
}
|
|
278
346
|
}
|
|
347
|
+
/** Query an already selected source, retaining npm proxy/CA/auth behavior. */
|
|
348
|
+
export async function queryPackageVersion(pkg, selection, signal) {
|
|
349
|
+
const registry = normalizeVersionRegistry(selection.registry);
|
|
350
|
+
if (!registry)
|
|
351
|
+
return null;
|
|
352
|
+
const version = selection.useNpmCli
|
|
353
|
+
? await checkLatestVersionViaNpm(pkg, 10000, signal, registry)
|
|
354
|
+
: await fetchLatestVersion(pkg, 10000, signal, registry);
|
|
355
|
+
return isPackageVersion(version) ? { version, registry } : null;
|
|
356
|
+
}
|
|
357
|
+
/** No registry lookup: callers must validate cache freshness before each install. */
|
|
358
|
+
export async function installExactPackageVersion(pkg, target, registry, resolveInstalled) {
|
|
359
|
+
if (![MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME].includes(pkg)
|
|
360
|
+
|| !isPackageVersion(target) || normalizeVersionRegistry(registry) !== registry) {
|
|
361
|
+
return { status: 'failed', error: 'Invalid cached install target' };
|
|
362
|
+
}
|
|
363
|
+
if (isLinkedInstall())
|
|
364
|
+
return { status: 'skipped' };
|
|
365
|
+
let result = { status: 'skipped' };
|
|
366
|
+
await withGlobalInstallLock(async () => {
|
|
367
|
+
const installed = resolveInstalled();
|
|
368
|
+
if (!installed || !isPackageVersion(installed.version))
|
|
369
|
+
return;
|
|
370
|
+
const from = installed.version;
|
|
371
|
+
if (comparePackageVersions(from, target) >= 0) {
|
|
372
|
+
result = { status: 'no-update', from };
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
376
|
+
try {
|
|
377
|
+
await npmInstallGlobal(`${pkg}@${target}`, { registry });
|
|
378
|
+
const actual = resolveInstalled()?.version;
|
|
379
|
+
if (!actual || !isPackageVersion(actual) || comparePackageVersions(actual, target) < 0) {
|
|
380
|
+
throw new Error('Installed version did not reach the cached target');
|
|
381
|
+
}
|
|
382
|
+
result = { status: 'upgraded', from, to: actual };
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// npm stderr can contain private registry credentials. Persist a safe
|
|
387
|
+
// diagnostic; npm's existing permission/transport behavior is retained.
|
|
388
|
+
result = { status: 'failed', from, to: target, error: 'Exact package installation or version verification failed' };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
279
394
|
/**
|
|
280
395
|
* Read the current npm registry first. If that read succeeds, npm itself is
|
|
281
396
|
* used for the version query so proxy/CA/auth settings are preserved. If the
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getPackageRoot, resolvePaths } from '../paths.js';
|
|
4
|
+
import { MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME, TARGET_PACKAGE_ROOT_ENV } from '../product.js';
|
|
5
|
+
import { getLocalVersion, isLinkedInstall, resolveGlobalPkg, resolveUpgradeRegistry, queryPackageVersion, installExactPackageVersion, } from './npm-ops.js';
|
|
6
|
+
import { comparePackageVersions, isPackageVersion, normalizeVersionRegistry } from './npm-ops.js';
|
|
7
|
+
import { atomicReplaceJson, withFileLock } from './atomic-write.js';
|
|
8
|
+
export const VERSION_INFO_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
export const VERSION_INFO_PACKAGES = [MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME, '@agentunion/fastaun'];
|
|
10
|
+
export const AUTO_UPGRADE_PACKAGES = [MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME];
|
|
11
|
+
const empty = () => ({ $schema_version: 1, packages: {} });
|
|
12
|
+
const timestamp = (value) => typeof value === 'string' ? Date.parse(value) : NaN;
|
|
13
|
+
const allowed = (name) => VERSION_INFO_PACKAGES.includes(name);
|
|
14
|
+
const filePath = () => path.join(resolvePaths().dataDir, 'package-version-info.json');
|
|
15
|
+
const writeLockPath = () => `${filePath()}.lock`;
|
|
16
|
+
/** Read-only: corrupt/unknown formats never recover fields from partial data. */
|
|
17
|
+
export function readPackageVersionInfo() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = JSON.parse(fs.readFileSync(filePath(), 'utf8'));
|
|
20
|
+
if (raw?.$schema_version !== 1 || !raw.packages || Array.isArray(raw.packages)
|
|
21
|
+
|| typeof raw.packages !== 'object' || Object.keys(raw.packages).some(name => !allowed(name)))
|
|
22
|
+
return empty();
|
|
23
|
+
const result = empty();
|
|
24
|
+
for (const [name, value] of Object.entries(raw.packages)) {
|
|
25
|
+
const entry = value;
|
|
26
|
+
if (!entry || entry.packageName !== name || !['ok', 'unavailable', 'skipped', 'not-installed'].includes(entry.lastAttemptStatus)
|
|
27
|
+
|| (entry.currentVersion !== null && !isPackageVersion(entry.currentVersion))
|
|
28
|
+
|| (entry.latestVersion != null && !isPackageVersion(entry.latestVersion))
|
|
29
|
+
|| (entry.registry !== undefined && normalizeVersionRegistry(entry.registry) !== entry.registry))
|
|
30
|
+
continue;
|
|
31
|
+
result.packages[name] = entry;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return empty();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Cooperating readers pause while a writer owns the Windows replace gate. */
|
|
40
|
+
export async function readPackageVersionInfoConsistent(timeoutMs = 30000) {
|
|
41
|
+
const deadline = Date.now() + timeoutMs;
|
|
42
|
+
while (fs.existsSync(writeLockPath())) {
|
|
43
|
+
if (Date.now() >= deadline)
|
|
44
|
+
return readPackageVersionInfo();
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, 15));
|
|
46
|
+
}
|
|
47
|
+
return readPackageVersionInfo();
|
|
48
|
+
}
|
|
49
|
+
export function isPackageVersionInfoFresh(entry, now = Date.now(), ttlMs = VERSION_INFO_TTL_MS, registry) {
|
|
50
|
+
if (!entry || !allowed(entry.packageName) || !isPackageVersion(entry.currentVersion)
|
|
51
|
+
|| !isPackageVersion(entry.latestVersion) || !entry.registry
|
|
52
|
+
|| normalizeVersionRegistry(entry.registry) !== entry.registry
|
|
53
|
+
|| (registry !== undefined && entry.registry !== registry))
|
|
54
|
+
return false;
|
|
55
|
+
const checked = timestamp(entry.checkedAt);
|
|
56
|
+
const observed = timestamp(entry.currentVersionObservedAt);
|
|
57
|
+
return Number.isFinite(checked) && checked <= now && now - checked <= ttlMs
|
|
58
|
+
&& Number.isFinite(observed) && observed <= now;
|
|
59
|
+
}
|
|
60
|
+
/** Snapshot groups have separate clocks so a stale query cannot undo an install. */
|
|
61
|
+
export async function mergePackageVersionInfo(patch) {
|
|
62
|
+
if (!allowed(patch.packageName))
|
|
63
|
+
throw new Error('Unmanaged package');
|
|
64
|
+
if (patch.currentVersion !== null && !isPackageVersion(patch.currentVersion))
|
|
65
|
+
throw new Error('Invalid installed version');
|
|
66
|
+
if (patch.checkedAt && (!isPackageVersion(patch.latestVersion)
|
|
67
|
+
|| !patch.registry || normalizeVersionRegistry(patch.registry) !== patch.registry))
|
|
68
|
+
throw new Error('Invalid successful snapshot');
|
|
69
|
+
const file = filePath();
|
|
70
|
+
return withFileLock(writeLockPath(), async () => {
|
|
71
|
+
const data = readPackageVersionInfo();
|
|
72
|
+
const previous = data.packages[patch.packageName];
|
|
73
|
+
const entry = previous ? { ...previous } : {
|
|
74
|
+
packageName: patch.packageName, currentVersion: null, lastAttemptStatus: patch.lastAttemptStatus,
|
|
75
|
+
};
|
|
76
|
+
const newer = (next, old) => Number.isFinite(timestamp(next))
|
|
77
|
+
&& (!Number.isFinite(timestamp(old)) || timestamp(old) > Date.now() || timestamp(next) >= timestamp(old));
|
|
78
|
+
if (newer(patch.currentVersionObservedAt, entry.currentVersionObservedAt)) {
|
|
79
|
+
entry.currentVersion = patch.currentVersion;
|
|
80
|
+
entry.currentVersionObservedAt = patch.currentVersionObservedAt;
|
|
81
|
+
}
|
|
82
|
+
if (newer(patch.checkedAt, entry.checkedAt)) {
|
|
83
|
+
entry.latestVersion = patch.latestVersion;
|
|
84
|
+
entry.checkedAt = patch.checkedAt;
|
|
85
|
+
entry.registry = patch.registry;
|
|
86
|
+
}
|
|
87
|
+
if (newer(patch.lastAttemptAt, entry.lastAttemptAt)) {
|
|
88
|
+
entry.lastAttemptStatus = patch.lastAttemptStatus;
|
|
89
|
+
entry.lastAttemptAt = patch.lastAttemptAt;
|
|
90
|
+
if (patch.lastError)
|
|
91
|
+
entry.lastError = patch.lastError;
|
|
92
|
+
else
|
|
93
|
+
delete entry.lastError;
|
|
94
|
+
}
|
|
95
|
+
if (patch.lastUpgrade && newer(patch.lastUpgrade.at, entry.lastUpgrade?.at))
|
|
96
|
+
entry.lastUpgrade = patch.lastUpgrade;
|
|
97
|
+
data.packages[patch.packageName] = entry;
|
|
98
|
+
await atomicReplaceJson(file, data);
|
|
99
|
+
return entry;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export function resolveVersionInfoPackage(name) {
|
|
103
|
+
if (!allowed(name))
|
|
104
|
+
throw new Error('Unmanaged package');
|
|
105
|
+
const root = process.env[TARGET_PACKAGE_ROOT_ENV] || getPackageRoot();
|
|
106
|
+
if (name === MAIN_PACKAGE_NAME)
|
|
107
|
+
return { version: getLocalVersion(), path: path.join(root, 'package.json') };
|
|
108
|
+
if (name === WEB_PACKAGE_NAME)
|
|
109
|
+
return resolveGlobalPkg(name);
|
|
110
|
+
const file = path.join(root, 'node_modules', '@agentunion', 'fastaun', 'package.json');
|
|
111
|
+
try {
|
|
112
|
+
return { version: JSON.parse(fs.readFileSync(file, 'utf8')).version, path: file };
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function hasUpdate(entry, registry, now = Date.now()) {
|
|
119
|
+
return isPackageVersionInfoFresh(entry, now, VERSION_INFO_TTL_MS, registry)
|
|
120
|
+
&& comparePackageVersions(entry.currentVersion, entry.latestVersion) < 0;
|
|
121
|
+
}
|
|
122
|
+
async function checkPackage(name, force, options) {
|
|
123
|
+
if (!allowed(name))
|
|
124
|
+
throw new Error('Unmanaged package');
|
|
125
|
+
const now = options.now ?? Date.now();
|
|
126
|
+
const at = new Date(now).toISOString();
|
|
127
|
+
const old = (await readPackageVersionInfoConsistent()).packages[name];
|
|
128
|
+
const observed = {
|
|
129
|
+
packageName: name, currentVersion: null, currentVersionObservedAt: at,
|
|
130
|
+
lastAttemptStatus: 'unavailable',
|
|
131
|
+
};
|
|
132
|
+
const log = (message) => options.log?.(`[${options.source ?? 'version-check'}] ${name}: ${message}`);
|
|
133
|
+
let source = 'refresh';
|
|
134
|
+
try {
|
|
135
|
+
const linked = isLinkedInstall();
|
|
136
|
+
const local = linked && name !== MAIN_PACKAGE_NAME ? null : resolveVersionInfoPackage(name);
|
|
137
|
+
if (local && !isPackageVersion(local.version))
|
|
138
|
+
throw new Error('Invalid installed package version');
|
|
139
|
+
observed.currentVersion = local?.version ?? null;
|
|
140
|
+
if (linked)
|
|
141
|
+
observed.lastAttemptStatus = 'skipped';
|
|
142
|
+
else if (!local)
|
|
143
|
+
observed.lastAttemptStatus = 'not-installed';
|
|
144
|
+
else {
|
|
145
|
+
const selection = options.selection ?? await resolveUpgradeRegistry(options.signal);
|
|
146
|
+
const registry = normalizeVersionRegistry(selection.registry);
|
|
147
|
+
if (!registry)
|
|
148
|
+
throw new Error('Registry configuration is not safe for version caching');
|
|
149
|
+
if (!force && isPackageVersionInfoFresh(old, now, VERSION_INFO_TTL_MS, registry)) {
|
|
150
|
+
source = 'cache';
|
|
151
|
+
observed.lastAttemptStatus = 'ok';
|
|
152
|
+
const entry = await mergePackageVersionInfo(observed);
|
|
153
|
+
log(`Using cached version information (age ${Math.floor((now - timestamp(old.checkedAt)) / 60000)}m)`);
|
|
154
|
+
return { packageName: name, status: 'ok', source, entry, hasUpdate: hasUpdate(entry, registry, now) };
|
|
155
|
+
}
|
|
156
|
+
log(force ? 'Refreshing version information' : 'Version information missing/invalid/expired; refreshing');
|
|
157
|
+
const checked = await queryPackageVersion(name, selection, options.signal);
|
|
158
|
+
if (!checked)
|
|
159
|
+
throw new Error('Version registry unavailable or returned an invalid version');
|
|
160
|
+
observed.latestVersion = checked.version;
|
|
161
|
+
observed.registry = checked.registry;
|
|
162
|
+
// Use request start, not completion: an older slow request cannot replace
|
|
163
|
+
// a later successful request, and query time consumes the TTL.
|
|
164
|
+
observed.checkedAt = at;
|
|
165
|
+
observed.lastAttemptStatus = 'ok';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
observed.lastAttemptStatus = 'unavailable';
|
|
170
|
+
observed.lastError = 'Version refresh failed; continuing with current version';
|
|
171
|
+
log(observed.lastError);
|
|
172
|
+
}
|
|
173
|
+
observed.lastAttemptAt = at;
|
|
174
|
+
let entry = { ...old, ...observed };
|
|
175
|
+
try {
|
|
176
|
+
entry = await mergePackageVersionInfo(observed);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
log('Version information could not be saved; continuing with current version');
|
|
180
|
+
return { packageName: name, source, entry, status: 'unavailable', hasUpdate: false, error: 'Version information could not be saved' };
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
packageName: name, source, entry, status: observed.lastAttemptStatus,
|
|
184
|
+
hasUpdate: observed.lastAttemptStatus === 'ok' && hasUpdate(entry, observed.registry, now),
|
|
185
|
+
...(observed.lastError ? { error: observed.lastError } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export async function refreshPackageVersionInfo(name, options = {}) {
|
|
189
|
+
return checkPackage(name, true, options);
|
|
190
|
+
}
|
|
191
|
+
export async function ensureFreshPackageVersionInfo(names = AUTO_UPGRADE_PACKAGES, options = {}) {
|
|
192
|
+
const selection = options.selection ?? (isLinkedInstall() ? undefined : await resolveUpgradeRegistry(options.signal));
|
|
193
|
+
return Promise.all(names.map(name => checkPackage(name, false, { ...options, selection })));
|
|
194
|
+
}
|
|
195
|
+
export async function refreshManagedPackageVersions(names = AUTO_UPGRADE_PACKAGES, options = {}) {
|
|
196
|
+
const selection = options.selection ?? (isLinkedInstall() ? undefined : await resolveUpgradeRegistry(options.signal));
|
|
197
|
+
return Promise.all(names.map(name => checkPackage(name, true, { ...options, selection })));
|
|
198
|
+
}
|
|
199
|
+
export async function recordPackageUpgrade(name, upgrade, log) {
|
|
200
|
+
try {
|
|
201
|
+
await mergePackageVersionInfo({
|
|
202
|
+
packageName: name, currentVersion: resolveVersionInfoPackage(name)?.version ?? null,
|
|
203
|
+
currentVersionObservedAt: new Date().toISOString(), lastAttemptStatus: 'unavailable', lastUpgrade: upgrade,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
log?.(`${name}: Failed to record install outcome`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Called only by the lifecycle coordinator, after shutdown. Never queries registry. */
|
|
211
|
+
export async function applyCachedPackageUpgrades(names = AUTO_UPGRADE_PACKAGES, options = {}) {
|
|
212
|
+
if (names.some(name => !AUTO_UPGRADE_PACKAGES.includes(name)))
|
|
213
|
+
throw new Error('Package is not automatically managed');
|
|
214
|
+
if (isLinkedInstall())
|
|
215
|
+
return names.map(() => ({ status: 'skipped' }));
|
|
216
|
+
const results = [];
|
|
217
|
+
for (const name of names) {
|
|
218
|
+
let result = { status: 'skipped' };
|
|
219
|
+
if (!AUTO_UPGRADE_PACKAGES.includes(name))
|
|
220
|
+
throw new Error('Package is not automatically managed');
|
|
221
|
+
try {
|
|
222
|
+
const entry = (await readPackageVersionInfoConsistent(0)).packages[name];
|
|
223
|
+
const registry = entry?.registry ? normalizeVersionRegistry(entry.registry) : null;
|
|
224
|
+
if (!registry || !isPackageVersionInfoFresh(entry, Date.now(), VERSION_INFO_TTL_MS, registry)) {
|
|
225
|
+
result = { status: 'skipped', error: 'No valid cached version information' };
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
options.log?.(`Applying cached version information: ${name}@${entry.latestVersion}`);
|
|
229
|
+
result = await installExactPackageVersion(name, entry.latestVersion, registry, () => resolveVersionInfoPackage(name));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
result = { status: 'failed', error: 'Cached upgrade could not acquire installation lock or read installed version' };
|
|
234
|
+
}
|
|
235
|
+
if (result.status === 'upgraded' || result.status === 'failed') {
|
|
236
|
+
await recordPackageUpgrade(name, {
|
|
237
|
+
from: result.from, to: result.to, at: new Date().toISOString(),
|
|
238
|
+
status: result.status === 'upgraded' ? 'installed' : 'failed', error: result.error,
|
|
239
|
+
}, options.log);
|
|
240
|
+
}
|
|
241
|
+
results.push(result);
|
|
242
|
+
}
|
|
243
|
+
return results;
|
|
244
|
+
}
|
|
@@ -36,6 +36,10 @@ export function isTrustedShellSignature(kind, evidence) {
|
|
|
36
36
|
function windowsSystemRoot() {
|
|
37
37
|
return process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
|
|
38
38
|
}
|
|
39
|
+
/** Return the only cmd.exe that EvolCore is willing to use as a shell carrier. */
|
|
40
|
+
export function windowsSystemCmdPath() {
|
|
41
|
+
return path.win32.join(windowsSystemRoot(), 'System32', 'cmd.exe');
|
|
42
|
+
}
|
|
39
43
|
function systemPowerShellPath() {
|
|
40
44
|
return path.win32.join(windowsSystemRoot(), 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
41
45
|
}
|
|
@@ -195,6 +199,39 @@ export function isTrustedWindowsShellExecutable(executable, kind) {
|
|
|
195
199
|
}
|
|
196
200
|
return trusted;
|
|
197
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Resolve an inherited ComSpec to the system cmd.exe and authenticate it.
|
|
204
|
+
* npm shims are not signed Microsoft binaries, so this helper intentionally
|
|
205
|
+
* validates only the interpreter and never the `.cmd`/`.bat` target.
|
|
206
|
+
*/
|
|
207
|
+
export function resolveTrustedWindowsCmdPath(comSpec) {
|
|
208
|
+
if (process.platform !== 'win32')
|
|
209
|
+
return comSpec || 'cmd.exe';
|
|
210
|
+
const systemCmd = windowsSystemCmdPath();
|
|
211
|
+
const candidate = (comSpec || process.env.ComSpec || process.env.COMSPEC || systemCmd).trim();
|
|
212
|
+
if (!candidate)
|
|
213
|
+
return undefined;
|
|
214
|
+
let candidatePath = candidate;
|
|
215
|
+
if (!isAbsoluteWindowsPath(candidatePath)) {
|
|
216
|
+
if (/^cmd(?:\.exe)?$/i.test(candidatePath))
|
|
217
|
+
candidatePath = systemCmd;
|
|
218
|
+
else
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
let candidateReal;
|
|
222
|
+
let systemReal;
|
|
223
|
+
try {
|
|
224
|
+
candidateReal = fs.realpathSync.native(candidatePath);
|
|
225
|
+
systemReal = fs.realpathSync.native(systemCmd);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
if (path.win32.normalize(candidateReal).toLowerCase() !== path.win32.normalize(systemReal).toLowerCase()) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
return isTrustedWindowsShellExecutable(systemReal, 'cmd') ? systemReal : undefined;
|
|
234
|
+
}
|
|
198
235
|
/** Test hook: clear cached signature decisions after a fixture is replaced. */
|
|
199
236
|
export function clearWindowsShellTrustCache() {
|
|
200
237
|
signatureCache.clear();
|
|
@@ -55,6 +55,14 @@ AUN keystore seed 由 EvolCore 使用固定应用值管理,不再提供配置
|
|
|
55
55
|
- **群 @ 处理策略**:`mention_mode=disabled|mention-only` 是群级配置,只决定哪些群消息进入 Agent LLM 上下文,不影响 AUN 协议层投递;被过滤消息仍应本地存档。
|
|
56
56
|
- **身份体系**:对端以 AID 标识,关系层 peerKey 形如 `aun#alice.aid.pub`。
|
|
57
57
|
|
|
58
|
+
### 私聊最终回复标题
|
|
59
|
+
|
|
60
|
+
AUN 私聊中,`result.text` 的 `isFinal=true` 会在 `payload.text` 正文前添加 `✅ 最终回复:`。AUN 文本客户端按正文展示,标题随 outbox 上下文持久化,断线恢复发送时仍保留。群聊、中间文本、命令回执及错误消息不自动添加该标题。
|
|
61
|
+
|
|
62
|
+
Interactive 私聊的最终正文保留到本轮结束后统一发送,避免定时 flush 提前当作中间文本发出。Codex 明确标记的 commentary 可按原定时窗口发送;没有阶段标记的正文等到下一文本块、工具调用或本轮结束后确定发送方式。标题和正文之间留一个空行,正文已经以同一标题开头时不重复添加,也不额外发送 `payload.title`。
|
|
63
|
+
|
|
64
|
+
标题仅表示最终回复的展示信息,任务完成或失败仍由 `task.status` 表达。
|
|
65
|
+
|
|
58
66
|
### 出站关注状态
|
|
59
67
|
|
|
60
68
|
AUN runner 状态和 action card 可携带顶层 `attention`,值只能是以下四种之一:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolcore",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
4
4
|
"description": "AI Agent gateway connecting Claude, Codex, Gemini, and the bundled ecagent runner to messaging channels with multi-project session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|