a2acalling 0.6.48 → 0.6.49
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/bin/cli.js +23 -0
- package/docs/plans/2026-02-16-auto-updater.md +1284 -0
- package/docs/plans/2026-02-16-e2e-test-prompt-sequence.md +3085 -0
- package/docs/plans/2026-02-17-claude-code-codex-skills.md +770 -0
- package/docs/prompts/e2e-test-agent.md +368 -0
- package/docs/protocol.md +79 -0
- package/package.json +1 -1
- package/src/dashboard/public/app.js +108 -1
- package/src/dashboard/public/index.html +9 -0
- package/src/dashboard/public/style.css +27 -0
- package/src/lib/config.js +41 -0
- package/src/lib/conversation-driver.js +62 -21
- package/src/lib/openclaw-integration.js +22 -66
- package/src/lib/summary-formatter.js +168 -0
- package/src/lib/summary-prompt.js +203 -0
- package/src/lib/update-checker.js +93 -0
- package/src/lib/update-manager.js +313 -0
- package/src/routes/a2a.js +8 -1
- package/src/routes/dashboard.js +103 -1
- package/src/server.js +115 -26
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Checker
|
|
3
|
+
*
|
|
4
|
+
* Zero-dependency npm version checks for a2acalling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/a2acalling/latest';
|
|
8
|
+
const FETCH_TIMEOUT_MS = 15000;
|
|
9
|
+
|
|
10
|
+
function parseVersion(str) {
|
|
11
|
+
if (!str || typeof str !== 'string') return null;
|
|
12
|
+
const match = str.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
13
|
+
if (!match) return null;
|
|
14
|
+
return {
|
|
15
|
+
major: Number.parseInt(match[1], 10),
|
|
16
|
+
minor: Number.parseInt(match[2], 10),
|
|
17
|
+
patch: Number.parseInt(match[3], 10)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function compareVersions(a, b) {
|
|
22
|
+
const va = parseVersion(a);
|
|
23
|
+
const vb = parseVersion(b);
|
|
24
|
+
if (!va || !vb) return 0;
|
|
25
|
+
if (va.major !== vb.major) return va.major < vb.major ? -1 : 1;
|
|
26
|
+
if (va.minor !== vb.minor) return va.minor < vb.minor ? -1 : 1;
|
|
27
|
+
if (va.patch !== vb.patch) return va.patch < vb.patch ? -1 : 1;
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isSameMajor(a, b) {
|
|
32
|
+
const va = parseVersion(a);
|
|
33
|
+
const vb = parseVersion(b);
|
|
34
|
+
if (!va || !vb) return false;
|
|
35
|
+
return va.major === vb.major;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function fetchLatestVersion() {
|
|
39
|
+
try {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
42
|
+
const res = await fetch(REGISTRY_URL, {
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
headers: { Accept: 'application/json' }
|
|
45
|
+
});
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
return { error: `Registry returned ${res.status}` };
|
|
49
|
+
}
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
if (!data || typeof data.version !== 'string') {
|
|
52
|
+
return { error: 'No version field in registry response' };
|
|
53
|
+
}
|
|
54
|
+
return { version: data.version };
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err && err.name === 'AbortError') {
|
|
57
|
+
return { error: 'Registry request timed out' };
|
|
58
|
+
}
|
|
59
|
+
return { error: err && err.message ? err.message : 'Unknown fetch error' };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function checkForUpdate(currentVersion) {
|
|
64
|
+
const result = await fetchLatestVersion();
|
|
65
|
+
if (result.error) {
|
|
66
|
+
return {
|
|
67
|
+
available: false,
|
|
68
|
+
current: currentVersion,
|
|
69
|
+
latest: null,
|
|
70
|
+
sameMajor: false,
|
|
71
|
+
error: result.error
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const latest = result.version;
|
|
76
|
+
return {
|
|
77
|
+
available: compareVersions(currentVersion, latest) < 0,
|
|
78
|
+
current: currentVersion,
|
|
79
|
+
latest,
|
|
80
|
+
sameMajor: isSameMajor(currentVersion, latest)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
REGISTRY_URL,
|
|
86
|
+
FETCH_TIMEOUT_MS,
|
|
87
|
+
parseVersion,
|
|
88
|
+
compareVersions,
|
|
89
|
+
isSameMajor,
|
|
90
|
+
fetchLatestVersion,
|
|
91
|
+
checkForUpdate
|
|
92
|
+
};
|
|
93
|
+
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
|
+
const { EventEmitter } = require('events');
|
|
3
|
+
const { createLogger } = require('./logger');
|
|
4
|
+
const { checkForUpdate, isSameMajor } = require('./update-checker');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_INTERVAL_MS = 60 * 60 * 1000;
|
|
7
|
+
const DEFAULT_NPM_TIMEOUT_MS = 2 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
function isUpdateSafe(callMonitor) {
|
|
10
|
+
if (!callMonitor || typeof callMonitor.getActiveCount !== 'function') return true;
|
|
11
|
+
return Number(callMonitor.getActiveCount()) === 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function shouldApplyUpdate(currentVersion, latestVersion, options = {}) {
|
|
15
|
+
if (!latestVersion) return false;
|
|
16
|
+
if (options.allowMajor) return true;
|
|
17
|
+
return isSameMajor(currentVersion, latestVersion);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function autoUpdateDisabledByEnv() {
|
|
21
|
+
if (String(process.env.CI || '').toLowerCase() === 'true') return true;
|
|
22
|
+
if (String(process.env.A2A_AUTO_UPDATE || '').trim() === '0') return true;
|
|
23
|
+
if (String(process.env.NO_AUTO_UPDATE || '').trim() === '1') return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function execFilePromise(cmd, args, options = {}) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
execFile(cmd, args, options, (err, stdout, stderr) => {
|
|
30
|
+
if (err) {
|
|
31
|
+
err.stdout = stdout;
|
|
32
|
+
err.stderr = stderr;
|
|
33
|
+
reject(err);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
resolve({ stdout, stderr });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class UpdateManager extends EventEmitter {
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
super();
|
|
44
|
+
this.currentVersion = options.currentVersion;
|
|
45
|
+
this.config = options.config || null;
|
|
46
|
+
this.logger = options.logger || createLogger({ component: 'a2a.updater' });
|
|
47
|
+
this.intervalMs = Number.isFinite(options.intervalMs) && options.intervalMs > 0
|
|
48
|
+
? options.intervalMs
|
|
49
|
+
: DEFAULT_INTERVAL_MS;
|
|
50
|
+
this.allowMajor = Boolean(options.allowMajor);
|
|
51
|
+
this.enabled = options.enabled !== false;
|
|
52
|
+
this.getCallMonitor = typeof options.getCallMonitor === 'function'
|
|
53
|
+
? options.getCallMonitor
|
|
54
|
+
: (() => null);
|
|
55
|
+
this.installTimeoutMs = Number.isFinite(options.installTimeoutMs) && options.installTimeoutMs > 0
|
|
56
|
+
? options.installTimeoutMs
|
|
57
|
+
: DEFAULT_NPM_TIMEOUT_MS;
|
|
58
|
+
this.restartFn = typeof options.restartFn === 'function' ? options.restartFn : null;
|
|
59
|
+
this._execFile = options.execFile || execFilePromise;
|
|
60
|
+
this._status = {
|
|
61
|
+
state: 'up_to_date',
|
|
62
|
+
enabled: this.enabled,
|
|
63
|
+
current_version: this.currentVersion,
|
|
64
|
+
latest_version: this.currentVersion,
|
|
65
|
+
target_version: null,
|
|
66
|
+
active_calls: 0,
|
|
67
|
+
last_checked_at: null,
|
|
68
|
+
last_success_at: null,
|
|
69
|
+
last_error: null,
|
|
70
|
+
defer_reason: null
|
|
71
|
+
};
|
|
72
|
+
this._timer = null;
|
|
73
|
+
this._running = false;
|
|
74
|
+
this._pendingManualUpdate = false;
|
|
75
|
+
|
|
76
|
+
this._loadConfig();
|
|
77
|
+
if (autoUpdateDisabledByEnv()) {
|
|
78
|
+
this.enabled = false;
|
|
79
|
+
this._status.enabled = false;
|
|
80
|
+
this.logger.info('Auto-update disabled by environment', { event: 'updater_env_disabled' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_loadConfig() {
|
|
85
|
+
if (!this.config || typeof this.config.getAutoUpdate !== 'function') return;
|
|
86
|
+
const cfg = this.config.getAutoUpdate();
|
|
87
|
+
if (cfg && typeof cfg === 'object') {
|
|
88
|
+
if (typeof cfg.enabled === 'boolean') this.enabled = cfg.enabled;
|
|
89
|
+
if (Number.isFinite(cfg.intervalMs) && cfg.intervalMs > 0) this.intervalMs = cfg.intervalMs;
|
|
90
|
+
if (typeof cfg.allowMajor === 'boolean') this.allowMajor = cfg.allowMajor;
|
|
91
|
+
}
|
|
92
|
+
this._status.enabled = this.enabled;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_persistConfigPatch(patch) {
|
|
96
|
+
if (!this.config || typeof this.config.setAutoUpdate !== 'function') return;
|
|
97
|
+
try {
|
|
98
|
+
this.config.setAutoUpdate(patch);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
this.logger.warn('Failed to persist auto-update config patch', {
|
|
101
|
+
event: 'updater_config_patch_failed',
|
|
102
|
+
error: err
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_setStatus(patch) {
|
|
108
|
+
this._status = { ...this._status, ...patch };
|
|
109
|
+
this.emit('status', this.getStatus());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getStatus() {
|
|
113
|
+
return {
|
|
114
|
+
...this._status,
|
|
115
|
+
enabled: this.enabled,
|
|
116
|
+
interval_ms: this.intervalMs,
|
|
117
|
+
allow_major: this.allowMajor
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async setEnabled(enabled) {
|
|
122
|
+
this.enabled = Boolean(enabled);
|
|
123
|
+
this._setStatus({ enabled: this.enabled });
|
|
124
|
+
this._persistConfigPatch({ enabled: this.enabled });
|
|
125
|
+
if (this.enabled && !this._timer) {
|
|
126
|
+
this.start();
|
|
127
|
+
await this.triggerCheck({ reason: 'manual_enable' });
|
|
128
|
+
}
|
|
129
|
+
if (!this.enabled && this._timer) {
|
|
130
|
+
this.stop();
|
|
131
|
+
}
|
|
132
|
+
return this.getStatus();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
start() {
|
|
136
|
+
if (this._timer) return;
|
|
137
|
+
if (!this.enabled) {
|
|
138
|
+
this.logger.info('Auto-updater disabled', { event: 'updater_disabled' });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this._timer = setInterval(() => {
|
|
143
|
+
this.triggerCheck({ reason: 'interval' }).catch((err) => {
|
|
144
|
+
this.logger.warn('Auto-update interval check failed', {
|
|
145
|
+
event: 'updater_interval_failed',
|
|
146
|
+
error: err
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}, this.intervalMs);
|
|
150
|
+
|
|
151
|
+
this.logger.info('Auto-updater started', {
|
|
152
|
+
event: 'updater_started',
|
|
153
|
+
data: {
|
|
154
|
+
interval_ms: this.intervalMs,
|
|
155
|
+
allow_major: this.allowMajor
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
stop() {
|
|
161
|
+
if (!this._timer) return;
|
|
162
|
+
clearInterval(this._timer);
|
|
163
|
+
this._timer = null;
|
|
164
|
+
this.logger.info('Auto-updater stopped', { event: 'updater_stopped' });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async triggerCheck(options = {}) {
|
|
168
|
+
return this._runCycle({ ...options, manualUpdate: false, forceCheck: Boolean(options.forceCheck) });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async triggerUpdate(options = {}) {
|
|
172
|
+
this._pendingManualUpdate = true;
|
|
173
|
+
return this._runCycle({ ...options, manualUpdate: true });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async _runCycle(options = {}) {
|
|
177
|
+
if (this._running) {
|
|
178
|
+
return this.getStatus();
|
|
179
|
+
}
|
|
180
|
+
if (!this.enabled && !options.manualUpdate && !options.forceCheck) {
|
|
181
|
+
return this.getStatus();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this._running = true;
|
|
185
|
+
const nowIso = new Date().toISOString();
|
|
186
|
+
this._setStatus({
|
|
187
|
+
state: 'checking',
|
|
188
|
+
last_checked_at: nowIso,
|
|
189
|
+
last_error: null,
|
|
190
|
+
defer_reason: null
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const result = await checkForUpdate(this.currentVersion);
|
|
195
|
+
if (result.error) {
|
|
196
|
+
this._setStatus({
|
|
197
|
+
state: 'failed',
|
|
198
|
+
latest_version: this.currentVersion,
|
|
199
|
+
last_error: result.error
|
|
200
|
+
});
|
|
201
|
+
return this.getStatus();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const latest = result.latest || this.currentVersion;
|
|
205
|
+
if (!result.available) {
|
|
206
|
+
this._setStatus({
|
|
207
|
+
state: 'up_to_date',
|
|
208
|
+
latest_version: latest,
|
|
209
|
+
target_version: null
|
|
210
|
+
});
|
|
211
|
+
return this.getStatus();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!shouldApplyUpdate(this.currentVersion, latest, { allowMajor: this.allowMajor })) {
|
|
215
|
+
this._setStatus({
|
|
216
|
+
state: 'up_to_date',
|
|
217
|
+
latest_version: latest,
|
|
218
|
+
target_version: null,
|
|
219
|
+
defer_reason: 'cross_major_blocked'
|
|
220
|
+
});
|
|
221
|
+
return this.getStatus();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const monitor = this.getCallMonitor();
|
|
225
|
+
const activeCalls = monitor && typeof monitor.getActiveCount === 'function'
|
|
226
|
+
? Number(monitor.getActiveCount()) || 0
|
|
227
|
+
: 0;
|
|
228
|
+
if (!isUpdateSafe(monitor) && !options.force) {
|
|
229
|
+
this._setStatus({
|
|
230
|
+
state: 'waiting_for_safe_restart',
|
|
231
|
+
latest_version: latest,
|
|
232
|
+
target_version: latest,
|
|
233
|
+
active_calls: activeCalls,
|
|
234
|
+
defer_reason: 'active_calls'
|
|
235
|
+
});
|
|
236
|
+
return this.getStatus();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this._setStatus({
|
|
240
|
+
state: 'downloading',
|
|
241
|
+
latest_version: latest,
|
|
242
|
+
target_version: latest,
|
|
243
|
+
active_calls: activeCalls
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await this._installVersion(latest);
|
|
247
|
+
this.currentVersion = latest;
|
|
248
|
+
this._persistConfigPatch({ lastGoodVersion: latest });
|
|
249
|
+
|
|
250
|
+
this._setStatus({
|
|
251
|
+
state: 'restarting',
|
|
252
|
+
current_version: latest,
|
|
253
|
+
latest_version: latest,
|
|
254
|
+
target_version: latest,
|
|
255
|
+
last_success_at: new Date().toISOString(),
|
|
256
|
+
last_error: null,
|
|
257
|
+
defer_reason: null,
|
|
258
|
+
active_calls: 0
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (this.restartFn) {
|
|
262
|
+
await this.restartFn(latest);
|
|
263
|
+
} else {
|
|
264
|
+
this._setStatus({
|
|
265
|
+
state: 'up_to_date',
|
|
266
|
+
current_version: latest,
|
|
267
|
+
latest_version: latest,
|
|
268
|
+
target_version: null
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return this.getStatus();
|
|
273
|
+
} catch (err) {
|
|
274
|
+
const message = err && err.message ? err.message : 'update_failed';
|
|
275
|
+
this._setStatus({
|
|
276
|
+
state: 'failed',
|
|
277
|
+
last_error: message
|
|
278
|
+
});
|
|
279
|
+
this.logger.error('Auto-update failed', {
|
|
280
|
+
event: 'updater_failed',
|
|
281
|
+
error: err
|
|
282
|
+
});
|
|
283
|
+
return this.getStatus();
|
|
284
|
+
} finally {
|
|
285
|
+
this._running = false;
|
|
286
|
+
this._pendingManualUpdate = false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async _installVersion(version) {
|
|
291
|
+
const args = ['install', '-g', `a2acalling@${version}`];
|
|
292
|
+
this._setStatus({ state: 'applying' });
|
|
293
|
+
this.logger.info('Installing auto-update target', {
|
|
294
|
+
event: 'updater_install_start',
|
|
295
|
+
data: { version }
|
|
296
|
+
});
|
|
297
|
+
await this._execFile('npm', args, {
|
|
298
|
+
timeout: this.installTimeoutMs,
|
|
299
|
+
env: process.env
|
|
300
|
+
});
|
|
301
|
+
this.logger.info('Auto-update install complete', {
|
|
302
|
+
event: 'updater_install_done',
|
|
303
|
+
data: { version }
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
DEFAULT_INTERVAL_MS,
|
|
310
|
+
isUpdateSafe,
|
|
311
|
+
shouldApplyUpdate,
|
|
312
|
+
UpdateManager
|
|
313
|
+
};
|
package/src/routes/a2a.js
CHANGED
|
@@ -174,17 +174,24 @@ function createRoutes(options = {}) {
|
|
|
174
174
|
maxDurationMs: options.maxDurationMs || 300000,
|
|
175
175
|
logger: logger.child({ component: 'a2a.call-monitor' })
|
|
176
176
|
});
|
|
177
|
+
if (typeof options.onCallMonitor === 'function') {
|
|
178
|
+
try {
|
|
179
|
+
options.onCallMonitor(monitor);
|
|
180
|
+
} catch (_) {}
|
|
181
|
+
}
|
|
177
182
|
|
|
178
183
|
/**
|
|
179
184
|
* GET /status
|
|
180
185
|
* Check if A2A is enabled
|
|
181
186
|
*/
|
|
182
187
|
router.get('/status', (req, res) => {
|
|
188
|
+
const activeCalls = monitor ? monitor.getActiveCount() : 0;
|
|
183
189
|
res.json({
|
|
184
190
|
a2a: true,
|
|
185
191
|
version: require('../../package.json').version,
|
|
186
192
|
capabilities: ['invoke', 'multi-turn'],
|
|
187
|
-
rate_limits: limits
|
|
193
|
+
rate_limits: limits,
|
|
194
|
+
active_calls: activeCalls
|
|
188
195
|
});
|
|
189
196
|
});
|
|
190
197
|
|
package/src/routes/dashboard.js
CHANGED
|
@@ -210,12 +210,45 @@ function buildContext(options = {}) {
|
|
|
210
210
|
config,
|
|
211
211
|
convStore,
|
|
212
212
|
callbookStore,
|
|
213
|
+
getUpdateManager: typeof options.getUpdateManager === 'function'
|
|
214
|
+
? options.getUpdateManager
|
|
215
|
+
: (() => null),
|
|
213
216
|
logger,
|
|
214
217
|
agentContext,
|
|
215
218
|
staticDir: DASHBOARD_STATIC_DIR
|
|
216
219
|
};
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
function resolveAutoUpdateStatus(context) {
|
|
223
|
+
const manager = context.getUpdateManager ? context.getUpdateManager() : null;
|
|
224
|
+
const config = context.config && typeof context.config.getAutoUpdate === 'function'
|
|
225
|
+
? context.config.getAutoUpdate()
|
|
226
|
+
: {
|
|
227
|
+
enabled: true,
|
|
228
|
+
intervalMs: 60 * 60 * 1000,
|
|
229
|
+
allowMajor: false,
|
|
230
|
+
lastGoodVersion: null
|
|
231
|
+
};
|
|
232
|
+
const runtime = manager && typeof manager.getStatus === 'function'
|
|
233
|
+
? manager.getStatus()
|
|
234
|
+
: null;
|
|
235
|
+
return {
|
|
236
|
+
enabled: runtime ? Boolean(runtime.enabled) : Boolean(config.enabled),
|
|
237
|
+
interval_ms: runtime && Number.isFinite(runtime.interval_ms) ? runtime.interval_ms : config.intervalMs,
|
|
238
|
+
allow_major: runtime ? Boolean(runtime.allow_major) : Boolean(config.allowMajor),
|
|
239
|
+
state: runtime ? runtime.state : 'up_to_date',
|
|
240
|
+
current_version: runtime ? runtime.current_version : require('../../package.json').version,
|
|
241
|
+
latest_version: runtime ? runtime.latest_version : null,
|
|
242
|
+
target_version: runtime ? runtime.target_version : null,
|
|
243
|
+
active_calls: runtime ? runtime.active_calls : 0,
|
|
244
|
+
last_checked_at: runtime ? runtime.last_checked_at : null,
|
|
245
|
+
last_success_at: runtime ? runtime.last_success_at : null,
|
|
246
|
+
last_error: runtime ? runtime.last_error : null,
|
|
247
|
+
defer_reason: runtime ? runtime.defer_reason : null,
|
|
248
|
+
last_good_version: config.lastGoodVersion || null
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
219
252
|
function buildContactIndex(contacts) {
|
|
220
253
|
const byName = new Map();
|
|
221
254
|
const byId = new Map();
|
|
@@ -537,7 +570,76 @@ function createDashboardApiRouter(options = {}) {
|
|
|
537
570
|
callbook: {
|
|
538
571
|
enabled: Boolean(context.callbookStore && context.callbookStore.isAvailable()),
|
|
539
572
|
device_count: Array.isArray(devices) ? devices.length : 0
|
|
540
|
-
}
|
|
573
|
+
},
|
|
574
|
+
auto_update: resolveAutoUpdateStatus(context)
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
router.get('/update/status', (req, res) => {
|
|
579
|
+
return res.json({
|
|
580
|
+
success: true,
|
|
581
|
+
auto_update: resolveAutoUpdateStatus(context)
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
router.post('/update/check', async (req, res) => {
|
|
586
|
+
const manager = context.getUpdateManager ? context.getUpdateManager() : null;
|
|
587
|
+
if (!manager || typeof manager.triggerCheck !== 'function') {
|
|
588
|
+
return res.status(503).json({
|
|
589
|
+
success: false,
|
|
590
|
+
error: 'updater_unavailable',
|
|
591
|
+
message: 'Auto-updater is not initialized for this server.'
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
await manager.triggerCheck({ reason: 'dashboard_manual_check', forceCheck: true });
|
|
595
|
+
return res.json({
|
|
596
|
+
success: true,
|
|
597
|
+
auto_update: resolveAutoUpdateStatus(context)
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
router.post('/update/now', async (req, res) => {
|
|
602
|
+
const manager = context.getUpdateManager ? context.getUpdateManager() : null;
|
|
603
|
+
if (!manager || typeof manager.triggerUpdate !== 'function') {
|
|
604
|
+
return res.status(503).json({
|
|
605
|
+
success: false,
|
|
606
|
+
error: 'updater_unavailable',
|
|
607
|
+
message: 'Auto-updater is not initialized for this server.'
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
const force = parseBoolean(req.body && (req.body.force !== undefined ? req.body.force : req.body.force_update));
|
|
611
|
+
await manager.triggerUpdate({
|
|
612
|
+
reason: 'dashboard_manual_update',
|
|
613
|
+
force
|
|
614
|
+
});
|
|
615
|
+
return res.json({
|
|
616
|
+
success: true,
|
|
617
|
+
auto_update: resolveAutoUpdateStatus(context)
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
router.put('/update/config', async (req, res) => {
|
|
622
|
+
const manager = context.getUpdateManager ? context.getUpdateManager() : null;
|
|
623
|
+
const body = req.body || {};
|
|
624
|
+
const enabled = body.enabled !== undefined ? parseBoolean(body.enabled) : undefined;
|
|
625
|
+
|
|
626
|
+
if (enabled === undefined) {
|
|
627
|
+
return res.status(400).json({
|
|
628
|
+
success: false,
|
|
629
|
+
error: 'enabled_required',
|
|
630
|
+
message: 'Pass { enabled: true|false }.'
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (manager && typeof manager.setEnabled === 'function') {
|
|
635
|
+
await manager.setEnabled(enabled);
|
|
636
|
+
} else if (context.config && typeof context.config.setAutoUpdate === 'function') {
|
|
637
|
+
context.config.setAutoUpdate({ enabled });
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return res.json({
|
|
641
|
+
success: true,
|
|
642
|
+
auto_update: resolveAutoUpdateStatus(context)
|
|
541
643
|
});
|
|
542
644
|
});
|
|
543
645
|
|