@tapicker/bridge 1.0.0 → 1.1.0
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/README.md +15 -15
- package/dist/bridge/bridge-service.d.ts +1 -1
- package/dist/bridge/bridge-service.js +7 -7
- package/dist/bridge/control.js +1 -1
- package/dist/bridge/export-store.d.ts +35 -0
- package/dist/bridge/export-store.js +195 -0
- package/dist/bridge/extension-session.d.ts +9 -2
- package/dist/bridge/extension-session.js +56 -8
- package/dist/cli.js +563 -148
- package/dist/commands.d.ts +8 -0
- package/dist/commands.js +276 -40
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +4 -2
- package/dist/http-client.d.ts +24 -0
- package/dist/http-client.js +101 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +4 -3
- package/dist/mcp.js +221 -134
- package/dist/protocol/schema.d.ts +503 -34
- package/dist/protocol/schema.js +396 -15
- package/dist/server.d.ts +7 -4
- package/dist/server.js +70 -26
- package/dist/update-notifier-worker.d.ts +2 -0
- package/dist/update-notifier-worker.js +3 -0
- package/dist/update-notifier.d.ts +9 -0
- package/dist/update-notifier.js +178 -0
- package/dist/workflow-watch.d.ts +8 -7
- package/dist/workflow-watch.js +39 -29
- package/package.json +16 -6
- package/docs/cli.md +0 -112
- package/docs/http-api.md +0 -149
- package/docs/mcp.md +0 -90
- package/docs/operations.md +0 -85
- package/docs/workflow-watch.md +0 -86
- package/src/bridge/bridge-service.ts +0 -291
- package/src/bridge/control.ts +0 -281
- package/src/bridge/daemon.ts +0 -102
- package/src/bridge/extension-session.ts +0 -286
- package/src/bridge/pairing-state.ts +0 -92
- package/src/cli.ts +0 -1019
- package/src/commands.ts +0 -158
- package/src/constants.ts +0 -10
- package/src/errors.ts +0 -72
- package/src/index.ts +0 -35
- package/src/mcp.ts +0 -386
- package/src/protocol/schema.ts +0 -202
- package/src/server.ts +0 -623
- package/src/workflow-watch.ts +0 -364
package/dist/server.js
CHANGED
|
@@ -7,13 +7,14 @@ import { join, resolve } from 'node:path';
|
|
|
7
7
|
import { WebSocketServer } from 'ws';
|
|
8
8
|
import { createMcpHandler } from '@modelcontextprotocol/server';
|
|
9
9
|
import { localhostHostValidation, toNodeHandler } from '@modelcontextprotocol/node';
|
|
10
|
-
import { DEFAULT_HOST, DEFAULT_MCP_PATH, DEFAULT_PORT, DEFAULT_PORT_MAX, DEFAULT_WS_PATH,
|
|
10
|
+
import { DEFAULT_HOST, DEFAULT_MCP_PATH, DEFAULT_PORT, DEFAULT_PORT_MAX, DEFAULT_WS_PATH, DEFAULT_WORKFLOW_STATUS_WATCH_WS_PATH, PROTOCOL_VERSION, VERSION, } from './constants.js';
|
|
11
11
|
import { BridgeService } from './bridge/bridge-service.js';
|
|
12
|
-
import { PairingStateFile } from './bridge/pairing-state.js';
|
|
12
|
+
import { getDefaultBridgeDataDir, PairingStateFile } from './bridge/pairing-state.js';
|
|
13
|
+
import { ExportStore } from './bridge/export-store.js';
|
|
13
14
|
import { getCommandCapabilities, dispatchCommand } from './commands.js';
|
|
14
15
|
import { BridgeError, safeError, safeSuccess, toBridgeError } from './errors.js';
|
|
15
16
|
import { createBridgeMcpApi, createTapickerMcpServer } from './mcp.js';
|
|
16
|
-
import {
|
|
17
|
+
import { WorkflowStatusWatchRequestSchema, createEnvelope, parseEnvelope } from './protocol/schema.js';
|
|
17
18
|
const DEFAULT_CORS_ORIGINS = ['*'];
|
|
18
19
|
const CORS_ALLOWED_METHODS = 'GET, POST, DELETE, OPTIONS';
|
|
19
20
|
const CORS_ALLOWED_HEADERS = 'Accept, Authorization, Content-Type, Last-Event-ID, MCP-Protocol-Version, Mcp-Session-Id';
|
|
@@ -25,6 +26,8 @@ export class BridgeServer {
|
|
|
25
26
|
requestedPort;
|
|
26
27
|
portMax;
|
|
27
28
|
token;
|
|
29
|
+
exports;
|
|
30
|
+
exportCleanupTimer;
|
|
28
31
|
corsOrigins;
|
|
29
32
|
tls;
|
|
30
33
|
mcpHandler;
|
|
@@ -41,28 +44,41 @@ export class BridgeServer {
|
|
|
41
44
|
this.token = options.token;
|
|
42
45
|
this.corsOrigins = normalizeCorsOrigins(options.corsOrigins ?? DEFAULT_CORS_ORIGINS);
|
|
43
46
|
this.tls = options.tls;
|
|
47
|
+
this.exports = new ExportStore(join(resolve(options.dataDir ?? getDefaultBridgeDataDir()), 'exports'));
|
|
44
48
|
const pairingState = options.dataDir
|
|
45
49
|
? new PairingStateFile(join(resolve(options.dataDir), 'paired-clients.json'))
|
|
46
50
|
: undefined;
|
|
47
51
|
this.bridge = new BridgeService({
|
|
48
52
|
pairingCode: options.pairingCode ?? String(randomInt(0, 10_000)).padStart(4, '0'),
|
|
49
53
|
capabilities: [
|
|
54
|
+
'workflow.guide',
|
|
55
|
+
'workflow.schema',
|
|
56
|
+
'workflow.validate',
|
|
57
|
+
'workflow.list',
|
|
58
|
+
'workflow.get',
|
|
59
|
+
'workflow.create',
|
|
60
|
+
'workflow.update',
|
|
50
61
|
'workflow.start',
|
|
51
62
|
'workflow.pause',
|
|
52
63
|
'workflow.resume',
|
|
53
64
|
'workflow.stop',
|
|
54
65
|
'workflow.status',
|
|
55
|
-
'workflow.
|
|
66
|
+
'workflow.logs',
|
|
67
|
+
'workflow.log',
|
|
68
|
+
'data.list',
|
|
69
|
+
'data.preview',
|
|
56
70
|
'data.get',
|
|
71
|
+
'data.export',
|
|
57
72
|
'data.delete',
|
|
73
|
+
'user.me',
|
|
58
74
|
],
|
|
59
75
|
pairingState,
|
|
60
76
|
});
|
|
61
|
-
this.mcpHandler = createMcpHandler(() => createTapickerMcpServer(createBridgeMcpApi(this.bridge)));
|
|
77
|
+
this.mcpHandler = createMcpHandler(() => createTapickerMcpServer(createBridgeMcpApi(this.bridge, this.exports)));
|
|
62
78
|
this.mcpNodeHandler = toNodeHandler(this.mcpHandler, {
|
|
63
79
|
onerror: (error) => console.error('Tapicker Bridge MCP request failed:', error),
|
|
64
80
|
});
|
|
65
|
-
this.bridge.on('workflow.status', (event) => this.publishWorkflowStatus(event));
|
|
81
|
+
this.bridge.on('workflow.status.changed', (event) => this.publishWorkflowStatus(event));
|
|
66
82
|
}
|
|
67
83
|
getPairingCode() {
|
|
68
84
|
return this.bridge.getPairingCode();
|
|
@@ -84,6 +100,9 @@ export class BridgeServer {
|
|
|
84
100
|
this.server = runtime.server;
|
|
85
101
|
this.webSocketServer = runtime.webSocketServer;
|
|
86
102
|
this.port = actualPort;
|
|
103
|
+
this.cleanExports();
|
|
104
|
+
this.exportCleanupTimer = setInterval(() => this.cleanExports(), 60_000);
|
|
105
|
+
this.exportCleanupTimer.unref();
|
|
87
106
|
return this.getAddress();
|
|
88
107
|
}
|
|
89
108
|
catch (error) {
|
|
@@ -99,6 +118,7 @@ export class BridgeServer {
|
|
|
99
118
|
throw toBridgeError(lastError, 'PortUnavailable', 503);
|
|
100
119
|
}
|
|
101
120
|
async close() {
|
|
121
|
+
clearInterval(this.exportCleanupTimer);
|
|
102
122
|
this.bridge.close();
|
|
103
123
|
this.workflowSubscriptions.clear();
|
|
104
124
|
await this.mcpHandler.close();
|
|
@@ -122,7 +142,7 @@ export class BridgeServer {
|
|
|
122
142
|
port: this.port,
|
|
123
143
|
httpUrl: `${protocol}://${displayHost}:${this.port}`,
|
|
124
144
|
wsUrl: `${webSocketProtocol}://${displayHost}:${this.port}${DEFAULT_WS_PATH}`,
|
|
125
|
-
|
|
145
|
+
workflowStatusWatchWsUrl: `${webSocketProtocol}://${displayHost}:${this.port}${DEFAULT_WORKFLOW_STATUS_WATCH_WS_PATH}`,
|
|
126
146
|
mcpUrl: `${protocol}://${displayHost}:${this.port}${DEFAULT_MCP_PATH}`,
|
|
127
147
|
};
|
|
128
148
|
}
|
|
@@ -134,7 +154,7 @@ export class BridgeServer {
|
|
|
134
154
|
}
|
|
135
155
|
createRuntime() {
|
|
136
156
|
const requestHandler = (request, response) => {
|
|
137
|
-
|
|
157
|
+
this.handleRequest(request, response);
|
|
138
158
|
};
|
|
139
159
|
const server = this.tls
|
|
140
160
|
? createHttpsServer({
|
|
@@ -145,7 +165,7 @@ export class BridgeServer {
|
|
|
145
165
|
const webSocketServer = new WebSocketServer({ noServer: true });
|
|
146
166
|
server.on('upgrade', (request, socket, head) => {
|
|
147
167
|
const requestUrl = new URL(request.url || '/', this.getOrigin(request));
|
|
148
|
-
if (requestUrl.pathname ===
|
|
168
|
+
if (requestUrl.pathname === DEFAULT_WORKFLOW_STATUS_WATCH_WS_PATH) {
|
|
149
169
|
try {
|
|
150
170
|
this.assertAuthorized(request);
|
|
151
171
|
}
|
|
@@ -153,7 +173,7 @@ export class BridgeServer {
|
|
|
153
173
|
rejectUpgrade(socket, toBridgeError(error));
|
|
154
174
|
return;
|
|
155
175
|
}
|
|
156
|
-
webSocketServer.handleUpgrade(request, socket, head, (client) => this.
|
|
176
|
+
webSocketServer.handleUpgrade(request, socket, head, (client) => this.attachWorkflowStatusWatcher(client));
|
|
157
177
|
return;
|
|
158
178
|
}
|
|
159
179
|
if (requestUrl.pathname !== DEFAULT_WS_PATH) {
|
|
@@ -166,10 +186,21 @@ export class BridgeServer {
|
|
|
166
186
|
});
|
|
167
187
|
return { server, webSocketServer };
|
|
168
188
|
}
|
|
189
|
+
cleanExports() {
|
|
190
|
+
this.exports.cleanup().catch((error) => console.warn('Failed to clean expired exports:', error));
|
|
191
|
+
}
|
|
169
192
|
async handleRequest(request, response) {
|
|
170
193
|
try {
|
|
171
194
|
const requestUrl = new URL(request.url || '/', this.getOrigin(request));
|
|
172
195
|
const method = request.method || 'GET';
|
|
196
|
+
const exportRoute = /^\/v1\/exports\/([0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})(\/upload)?$/.exec(requestUrl.pathname);
|
|
197
|
+
// 上传由当前导出的一次性凭证授权,与浏览器页面的 CORS 配置独立。
|
|
198
|
+
if (method === 'POST' && exportRoute?.[2]) {
|
|
199
|
+
await this.exports.upload(exportRoute[1], request);
|
|
200
|
+
response.statusCode = 204;
|
|
201
|
+
response.end();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
173
204
|
if (!this.applyCors(request, response)) {
|
|
174
205
|
this.sendJson(response, 403, safeError(new BridgeError('CorsOriginNotAllowed', 'Cross-origin requests from this Origin are not allowed', {
|
|
175
206
|
status: 403,
|
|
@@ -189,6 +220,11 @@ export class BridgeServer {
|
|
|
189
220
|
await this.mcpNodeHandler(request, response);
|
|
190
221
|
return;
|
|
191
222
|
}
|
|
223
|
+
if (method === 'GET' && exportRoute && !exportRoute[2]) {
|
|
224
|
+
this.assertAuthorized(request);
|
|
225
|
+
await this.exports.download(exportRoute[1], response);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
192
228
|
if (method === 'GET' && requestUrl.pathname === '/v1/health') {
|
|
193
229
|
this.sendJson(response, 200, {
|
|
194
230
|
ok: true,
|
|
@@ -206,7 +242,7 @@ export class BridgeServer {
|
|
|
206
242
|
protocolVersion: PROTOCOL_VERSION,
|
|
207
243
|
bridgeId: this.bridgeId,
|
|
208
244
|
wsPath: DEFAULT_WS_PATH,
|
|
209
|
-
|
|
245
|
+
workflowStatusWatchPath: DEFAULT_WORKFLOW_STATUS_WATCH_WS_PATH,
|
|
210
246
|
requiresAuthentication: Boolean(this.token),
|
|
211
247
|
});
|
|
212
248
|
return;
|
|
@@ -225,22 +261,30 @@ export class BridgeServer {
|
|
|
225
261
|
});
|
|
226
262
|
return;
|
|
227
263
|
}
|
|
228
|
-
if (requestUrl.pathname ===
|
|
264
|
+
if (requestUrl.pathname === DEFAULT_WORKFLOW_STATUS_WATCH_WS_PATH) {
|
|
229
265
|
this.assertAuthorized(request);
|
|
230
|
-
this.sendJson(response, 405, safeError(new BridgeError('WebSocketRequired', 'workflow.watch requires a WebSocket connection', {
|
|
266
|
+
this.sendJson(response, 405, safeError(new BridgeError('WebSocketRequired', 'workflow.status.watch requires a WebSocket connection', {
|
|
267
|
+
status: 405,
|
|
268
|
+
})));
|
|
231
269
|
return;
|
|
232
270
|
}
|
|
233
271
|
if (method === 'POST' && requestUrl.pathname.startsWith('/v1/commands/')) {
|
|
234
272
|
this.assertAuthorized(request);
|
|
235
273
|
const commandName = decodeURIComponent(requestUrl.pathname.slice('/v1/commands/'.length));
|
|
236
274
|
const body = await readJson(request);
|
|
237
|
-
const data = await dispatchCommand(commandName, body, { bridge: this.bridge });
|
|
275
|
+
const data = await dispatchCommand(commandName, body, { bridge: this.bridge, exports: this.exports });
|
|
238
276
|
this.sendJson(response, 200, safeSuccess(data));
|
|
239
277
|
return;
|
|
240
278
|
}
|
|
241
279
|
this.sendJson(response, 404, safeError(new BridgeError('NotFound', 'Route not found', { status: 404 })));
|
|
242
280
|
}
|
|
243
281
|
catch (error) {
|
|
282
|
+
if (response.destroyed)
|
|
283
|
+
return;
|
|
284
|
+
if (response.headersSent) {
|
|
285
|
+
response.destroy();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
244
288
|
const bridgeError = toBridgeError(error);
|
|
245
289
|
this.sendJson(response, bridgeError.status, safeError(bridgeError));
|
|
246
290
|
}
|
|
@@ -255,33 +299,33 @@ export class BridgeServer {
|
|
|
255
299
|
throw new BridgeError('Unauthorized', 'Bridge authentication is required', { status: 401 });
|
|
256
300
|
}
|
|
257
301
|
}
|
|
258
|
-
|
|
302
|
+
attachWorkflowStatusWatcher(socket) {
|
|
259
303
|
const remove = () => this.workflowSubscriptions.delete(socket);
|
|
260
304
|
socket.on('close', remove);
|
|
261
305
|
socket.on('error', remove);
|
|
262
306
|
socket.on('message', (raw) => {
|
|
263
307
|
const parsedEnvelope = parseEnvelope(raw);
|
|
264
308
|
if (!parsedEnvelope.success) {
|
|
265
|
-
socket.close(1007, 'Invalid Workflow watch message');
|
|
309
|
+
socket.close(1007, 'Invalid Workflow status watch message');
|
|
266
310
|
return;
|
|
267
311
|
}
|
|
268
312
|
const request = parsedEnvelope.data;
|
|
269
|
-
if (request.type !== 'workflow.watch') {
|
|
270
|
-
this.
|
|
313
|
+
if (request.type !== 'workflow.status.watch') {
|
|
314
|
+
this.sendWorkflowStatusWatchResult(socket, request.id, safeError(new BridgeError('UnsupportedMessage', 'Only workflow.status.watch is supported on this socket', {
|
|
271
315
|
status: 400,
|
|
272
316
|
})));
|
|
273
317
|
return;
|
|
274
318
|
}
|
|
275
|
-
const parsedRequest =
|
|
319
|
+
const parsedRequest = WorkflowStatusWatchRequestSchema.safeParse(request.data);
|
|
276
320
|
if (!parsedRequest.success) {
|
|
277
|
-
this.
|
|
321
|
+
this.sendWorkflowStatusWatchResult(socket, request.id, safeError(new BridgeError('InvalidArguments', 'Invalid Workflow status watch arguments', {
|
|
278
322
|
status: 400,
|
|
279
323
|
details: parsedRequest.error.issues,
|
|
280
324
|
})));
|
|
281
325
|
return;
|
|
282
326
|
}
|
|
283
327
|
if (this.workflowSubscriptions.has(socket)) {
|
|
284
|
-
this.
|
|
328
|
+
this.sendWorkflowStatusWatchResult(socket, request.id, safeError(new BridgeError('WorkflowStatusWatchAlreadyStarted', 'This socket is already watching a Workflow', {
|
|
285
329
|
status: 409,
|
|
286
330
|
})));
|
|
287
331
|
return;
|
|
@@ -291,17 +335,17 @@ export class BridgeServer {
|
|
|
291
335
|
const clientId = this.bridge.resolveCommandClientId('workflow.status', watchRequest.clientId);
|
|
292
336
|
const subscription = { clientId, workflowId: watchRequest.input.workflowId };
|
|
293
337
|
this.workflowSubscriptions.set(socket, subscription);
|
|
294
|
-
this.
|
|
338
|
+
this.sendWorkflowStatusWatchResult(socket, request.id, safeSuccess(subscription));
|
|
295
339
|
}
|
|
296
340
|
catch (error) {
|
|
297
|
-
this.
|
|
341
|
+
this.sendWorkflowStatusWatchResult(socket, request.id, safeError(error));
|
|
298
342
|
}
|
|
299
343
|
});
|
|
300
344
|
}
|
|
301
|
-
|
|
345
|
+
sendWorkflowStatusWatchResult(socket, id, data) {
|
|
302
346
|
if (socket.readyState === socket.OPEN) {
|
|
303
347
|
try {
|
|
304
|
-
socket.send(JSON.stringify(createEnvelope('workflow.watch:res', data, { id, callback: false })));
|
|
348
|
+
socket.send(JSON.stringify(createEnvelope('workflow.status.watch:res', data, { id, callback: false })));
|
|
305
349
|
}
|
|
306
350
|
catch {
|
|
307
351
|
this.workflowSubscriptions.delete(socket);
|
|
@@ -309,7 +353,7 @@ export class BridgeServer {
|
|
|
309
353
|
}
|
|
310
354
|
}
|
|
311
355
|
publishWorkflowStatus(event) {
|
|
312
|
-
const envelope = JSON.stringify(createEnvelope('workflow.status', event, { callback: false }));
|
|
356
|
+
const envelope = JSON.stringify(createEnvelope('workflow.status.changed', event, { callback: false }));
|
|
313
357
|
for (const [socket, subscription] of this.workflowSubscriptions) {
|
|
314
358
|
if (socket.readyState === socket.OPEN && subscriptionKey(subscription) === subscriptionKey(event)) {
|
|
315
359
|
try {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从缓存显示提醒,然后按需启动独立的检查进程。
|
|
3
|
+
*/
|
|
4
|
+
export declare function showCachedUpdate(directory?: string): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* 在后台进程中刷新 npm 缓存。失败也记录检查时间,避免离线时反复请求。
|
|
7
|
+
*/
|
|
8
|
+
export declare function refreshUpdateCache(directory?: string): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=update-notifier.d.ts.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { exec, spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, rename, rmdir, stat, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { getDefaultBridgeDataDir } from './bridge/pairing-state.js';
|
|
9
|
+
import { VERSION } from './constants.js';
|
|
10
|
+
const UPDATE_INTERVAL = 24 * 60 * 60 * 1000;
|
|
11
|
+
const REQUEST_TIMEOUT = 5000;
|
|
12
|
+
const LOCK_TIMEOUT = 60_000;
|
|
13
|
+
const DEFAULT_REGISTRY = 'https://registry.npmjs.org/';
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
const CacheSchema = z.object({
|
|
16
|
+
checkedAt: z.number().optional(),
|
|
17
|
+
latestVersion: z.string().optional(),
|
|
18
|
+
notifiedAt: z.number().optional(),
|
|
19
|
+
});
|
|
20
|
+
const RegistrySchema = z.object({ 'dist-tags': z.object({ latest: z.string() }) });
|
|
21
|
+
const numericVersion = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
|
|
22
|
+
/**
|
|
23
|
+
* 仅比较发布流程使用的三段数字版本。
|
|
24
|
+
*/
|
|
25
|
+
function isNewerVersion(latest, current) {
|
|
26
|
+
if (!numericVersion.test(latest) || !numericVersion.test(current))
|
|
27
|
+
return false;
|
|
28
|
+
const latestParts = latest.split('.').map(BigInt);
|
|
29
|
+
const currentParts = current.split('.').map(BigInt);
|
|
30
|
+
const difference = latestParts.findIndex((part, index) => part !== currentParts[index]);
|
|
31
|
+
return difference !== -1 && latestParts[difference] > currentParts[difference];
|
|
32
|
+
}
|
|
33
|
+
function isRecent(timestamp) {
|
|
34
|
+
return timestamp !== undefined && Date.now() - timestamp < UPDATE_INTERVAL;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 使用与全局升级命令相同的 npm 配置,并保留镜像地址的路径前缀。
|
|
38
|
+
*/
|
|
39
|
+
async function getRegistryPackageUrl() {
|
|
40
|
+
const config = await execAsync('npm config get @tapicker:registry registry --global', {
|
|
41
|
+
timeout: 2000,
|
|
42
|
+
windowsHide: true,
|
|
43
|
+
}).catch(() => undefined);
|
|
44
|
+
const settings = config?.stdout.trim().split(/\r?\n/) ?? [];
|
|
45
|
+
const registry = ['@tapicker:registry', 'registry']
|
|
46
|
+
.map((key) => settings
|
|
47
|
+
.find((line) => line.startsWith(`${key}=`))
|
|
48
|
+
?.slice(key.length + 1)
|
|
49
|
+
.trim())
|
|
50
|
+
.find((value) => value && value !== 'undefined' && value !== 'null');
|
|
51
|
+
const url = new URL(registry ?? DEFAULT_REGISTRY);
|
|
52
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
53
|
+
throw new Error('Unsupported npm registry protocol');
|
|
54
|
+
}
|
|
55
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}/@tapicker%2Fbridge`;
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
58
|
+
async function readCache(path) {
|
|
59
|
+
try {
|
|
60
|
+
return CacheSchema.safeParse(JSON.parse(await readFile(path, 'utf8'))).data ?? {};
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof SyntaxError || error.code === 'ENOENT')
|
|
64
|
+
return {};
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function writeCache(path, cache) {
|
|
69
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
70
|
+
try {
|
|
71
|
+
await writeFile(temporaryPath, JSON.stringify(cache), { flag: 'wx', mode: 0o600 });
|
|
72
|
+
await rename(temporaryPath, path);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 尝试获取短期文件锁。已有进程持锁时直接跳过,避免 CLI 等待网络请求。
|
|
80
|
+
*/
|
|
81
|
+
async function withCacheLock(directory, action) {
|
|
82
|
+
const path = join(directory, 'update-notifier.json');
|
|
83
|
+
const lockPath = `${path}.lock`;
|
|
84
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
85
|
+
try {
|
|
86
|
+
await mkdir(lockPath);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error.code !== 'EEXIST')
|
|
90
|
+
throw error;
|
|
91
|
+
const lock = await stat(lockPath).catch(() => undefined);
|
|
92
|
+
if (!lock || Date.now() - lock.mtimeMs < LOCK_TIMEOUT)
|
|
93
|
+
return;
|
|
94
|
+
await rmdir(lockPath);
|
|
95
|
+
await mkdir(lockPath);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await action(path);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await rmdir(lockPath);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function printUpdate(latest) {
|
|
105
|
+
const useColor = !('NO_COLOR' in process.env) && process.stderr.hasColors?.();
|
|
106
|
+
const title = `Update Available: ${VERSION} → `;
|
|
107
|
+
const lines = [`${title}${latest}`, 'npm i -g @tapicker/bridge@latest', 'Restart Bridge to Apply if Running'];
|
|
108
|
+
const width = Math.max(...lines.map((line) => line.length));
|
|
109
|
+
const border = '─'.repeat(width + 4);
|
|
110
|
+
const rows = lines.map((line, index) => {
|
|
111
|
+
const padding = width - line.length;
|
|
112
|
+
const left = Math.floor(padding / 2);
|
|
113
|
+
const right = padding - left;
|
|
114
|
+
const label = index === 0 && useColor ? `${title}\x1b[36m${latest}\x1b[0m` : line;
|
|
115
|
+
return `│${' '.repeat(left + 2)}${label}${' '.repeat(right + 2)}│`;
|
|
116
|
+
});
|
|
117
|
+
console.error([`╭${border}╮`, ...rows, `╰${border}╯`].join('\n'));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 从缓存显示提醒,然后按需启动独立的检查进程。
|
|
121
|
+
*/
|
|
122
|
+
export async function showCachedUpdate(directory = getDefaultBridgeDataDir()) {
|
|
123
|
+
try {
|
|
124
|
+
const cache = await readCache(join(directory, 'update-notifier.json'));
|
|
125
|
+
const latestVersion = cache.latestVersion;
|
|
126
|
+
if (latestVersion && isNewerVersion(latestVersion, VERSION) && !isRecent(cache.notifiedAt)) {
|
|
127
|
+
await withCacheLock(directory, async (path) => {
|
|
128
|
+
const current = await readCache(path);
|
|
129
|
+
if (current.latestVersion !== latestVersion || isRecent(current.notifiedAt))
|
|
130
|
+
return;
|
|
131
|
+
await writeCache(path, { ...current, notifiedAt: Date.now() });
|
|
132
|
+
printUpdate(latestVersion);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (!isRecent(cache.checkedAt)) {
|
|
136
|
+
const worker = fileURLToPath(new URL('./update-notifier-worker.js', import.meta.url));
|
|
137
|
+
const child = spawn(process.execPath, [worker, directory], {
|
|
138
|
+
detached: true,
|
|
139
|
+
stdio: 'ignore',
|
|
140
|
+
windowsHide: true,
|
|
141
|
+
});
|
|
142
|
+
child.on('error', () => undefined);
|
|
143
|
+
child.unref();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// 缓存或进程创建失败时,继续执行 CLI 命令。
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 在后台进程中刷新 npm 缓存。失败也记录检查时间,避免离线时反复请求。
|
|
152
|
+
*/
|
|
153
|
+
export async function refreshUpdateCache(directory = getDefaultBridgeDataDir()) {
|
|
154
|
+
await withCacheLock(directory, async (path) => {
|
|
155
|
+
const cache = await readCache(path);
|
|
156
|
+
if (isRecent(cache.checkedAt))
|
|
157
|
+
return;
|
|
158
|
+
const checkedAt = Date.now();
|
|
159
|
+
let latestVersion;
|
|
160
|
+
try {
|
|
161
|
+
const response = await fetch(await getRegistryPackageUrl(), {
|
|
162
|
+
headers: { Accept: 'application/vnd.npm.install-v1+json' },
|
|
163
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
|
|
164
|
+
});
|
|
165
|
+
if (response.ok) {
|
|
166
|
+
const result = RegistrySchema.safeParse(await response.json());
|
|
167
|
+
const version = result.data?.['dist-tags'].latest;
|
|
168
|
+
if (version && numericVersion.test(version))
|
|
169
|
+
latestVersion = version;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// 网络错误与超时都不需要提示。
|
|
174
|
+
}
|
|
175
|
+
await writeCache(path, { ...cache, checkedAt, latestVersion });
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=update-notifier.js.map
|
package/dist/workflow-watch.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { BridgeError } from './errors.js';
|
|
2
2
|
import type { WorkflowStatusEvent } from './protocol/schema.js';
|
|
3
|
-
export type
|
|
3
|
+
export type WorkflowStatusWatchInput = {
|
|
4
4
|
clientId?: string;
|
|
5
5
|
workflowId: string;
|
|
6
6
|
timeoutMs?: number;
|
|
7
7
|
};
|
|
8
|
-
export type
|
|
9
|
-
|
|
8
|
+
export type WorkflowStatusWatchResult = WorkflowStatusEvent & {
|
|
9
|
+
isTimedOut: boolean;
|
|
10
10
|
};
|
|
11
|
-
export type
|
|
11
|
+
export type WorkflowStatusWatchConnection = {
|
|
12
12
|
clientId: string;
|
|
13
13
|
workflowId: string;
|
|
14
14
|
onStatus: (listener: (event: WorkflowStatusEvent) => void) => () => void;
|
|
@@ -18,14 +18,15 @@ export type WorkflowWatchConnection = {
|
|
|
18
18
|
type WaitForWorkflowStatusOptions = {
|
|
19
19
|
clientId: string;
|
|
20
20
|
workflowId: string;
|
|
21
|
+
runAt?: string;
|
|
21
22
|
subscribe: (onStatus: (event: WorkflowStatusEvent) => void, onClose: (error: BridgeError) => void) => () => void;
|
|
22
23
|
readStatus: () => Promise<WorkflowStatusEvent>;
|
|
23
24
|
onStatus?: (event: WorkflowStatusEvent) => void;
|
|
24
25
|
timeoutMs?: number;
|
|
25
26
|
signal?: AbortSignal;
|
|
26
27
|
};
|
|
27
|
-
export declare function
|
|
28
|
-
export declare function
|
|
29
|
-
export declare function waitForWorkflowStatus(options: WaitForWorkflowStatusOptions): Promise<
|
|
28
|
+
export declare function workflowStatusWatchWebSocketUrl(bridgeUrl: URL): import("url").URL;
|
|
29
|
+
export declare function connectWorkflowStatusWatch(url: string | URL, input: Omit<WorkflowStatusWatchInput, 'timeoutMs'>, token?: string, signal?: AbortSignal): Promise<WorkflowStatusWatchConnection>;
|
|
30
|
+
export declare function waitForWorkflowStatus(options: WaitForWorkflowStatusOptions): Promise<WorkflowStatusWatchResult>;
|
|
30
31
|
export {};
|
|
31
32
|
//# sourceMappingURL=workflow-watch.d.ts.map
|