@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/cli.js
CHANGED
|
@@ -1,33 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cac } from 'cac';
|
|
3
|
-
import { chmodSync, closeSync, openSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, closeSync, constants, openSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { fork } from 'node:child_process';
|
|
5
|
-
import { mkdir } from 'node:fs/promises';
|
|
6
|
-
import {
|
|
5
|
+
import { access, mkdir, open, rm, stat } from 'node:fs/promises';
|
|
6
|
+
import { Readable } from 'node:stream';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import { join, parse, resolve, sep } from 'node:path';
|
|
7
9
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
8
10
|
import { DEFAULT_HOST, DEFAULT_PORT, VERSION } from './constants.js';
|
|
9
11
|
import { BridgeError, serializeBridgeError } from './errors.js';
|
|
10
12
|
import { createHttpMcpApi, createTapickerMcpServer } from './mcp.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
+
import { createHttpBridgeApi, parseBridgeUrl } from './http-client.js';
|
|
14
|
+
import { DataInputSchema, DataGetInputSchema, DataExportInputSchema, DataExportResultSchema, DataListInputSchema, DataPreviewInputSchema, WorkflowControlInputSchema, WorkflowStartInputSchema, WorkflowStartResultSchema, WorkflowStatusEventSchema, WorkflowStatusInputSchema, WorkflowLogsInputSchema, WorkflowLogInputSchema, WorkflowListInputSchema, WorkflowGetInputSchema, WorkflowCreateInputSchema, WorkflowUpdateInputSchema, WorkflowGuideInputSchema, WorkflowSchemaInputSchema, WorkflowValidateInputSchema, } from './protocol/schema.js';
|
|
15
|
+
import { connectWorkflowStatusWatch, waitForWorkflowStatus, workflowStatusWatchWebSocketUrl } from './workflow-watch.js';
|
|
13
16
|
import { BridgeServer } from './server.js';
|
|
17
|
+
import { showCachedUpdate } from './update-notifier.js';
|
|
14
18
|
import { getDefaultBridgeDataDir } from './bridge/pairing-state.js';
|
|
15
19
|
import { sendBridgeControlRequest, startBridgeControlServer } from './bridge/control.js';
|
|
16
20
|
import { getBridgeDaemonLogPath, isProcessRunning, readBridgeDaemonState, removeBridgeDaemonState, writeBridgeDaemonState, } from './bridge/daemon.js';
|
|
17
21
|
const DAEMON_CHILD_ENV = 'TAPICKER_BRIDGE_DAEMON_CHILD';
|
|
18
22
|
const cli = cac('tapicker');
|
|
23
|
+
const defaultBridgeUrl = process.env.TAPICKER_BRIDGE_URL || `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;
|
|
19
24
|
cli
|
|
20
25
|
.command('bridge <action>', 'Manage Bridge: start/stop it, view the pairing code, or list connected Extensions')
|
|
21
|
-
.option('--host <host>', 'Host to bind'
|
|
26
|
+
.option('--host <host>', 'Host to bind')
|
|
22
27
|
.option('--port <port>', 'Port to bind; defaults to the first available port from 9520 to 9530')
|
|
23
28
|
.option('--token <token>', 'Bearer token for HTTP command clients')
|
|
24
|
-
.option('--cors-origin <origins>', 'Comma-separated allowed HTTP origins (default: *)'
|
|
29
|
+
.option('--cors-origin <origins>', 'Comma-separated allowed HTTP origins (default: *)')
|
|
25
30
|
.option('--tls-key <path>', 'TLS private key path')
|
|
26
31
|
.option('--tls-cert <path>', 'TLS certificate path')
|
|
27
32
|
.option('--data-dir <path>', 'Directory for Bridge state and logs')
|
|
28
33
|
.option('--daemon', 'Run Bridge as a daemon')
|
|
29
34
|
.option('--refresh', 'Rotate the pairing code and unlock pairing')
|
|
30
35
|
.action(async (action, options) => {
|
|
36
|
+
validateActionOptions(action, options, bridgeCommonOptions, bridgeActionOptions);
|
|
31
37
|
if (action === 'start') {
|
|
32
38
|
if (options.daemon && process.env[DAEMON_CHILD_ENV] !== '1') {
|
|
33
39
|
await startBridgeInBackground(options);
|
|
@@ -52,61 +58,136 @@ cli
|
|
|
52
58
|
throw new BridgeError('CommandNotFound', `Unsupported bridge action: ${action}`, { status: 404 });
|
|
53
59
|
});
|
|
54
60
|
cli
|
|
55
|
-
.command('workflow <action>
|
|
56
|
-
.option('--
|
|
57
|
-
.option('--
|
|
58
|
-
.option('--host <host>', '
|
|
59
|
-
.option('--
|
|
61
|
+
.command('workflow <action> [target] [logId]', 'Read guides, validate nodes, list/get/create/update Workflows, control execution, or query logs')
|
|
62
|
+
.option('--scope <scope>', 'Workflow list scope: own, shared or pinned (list only)')
|
|
63
|
+
.option('--name <name>', 'Match a substring of Workflow full names (list only)')
|
|
64
|
+
.option('--host <host>', 'Match Workflow host exactly, without a scheme or path (list only)')
|
|
65
|
+
.option('--page <page>', 'Page number (list only; default: 1)')
|
|
66
|
+
.option('--page-size <count>', 'Page size, 1–100 (list only; default: 20)')
|
|
67
|
+
.option('--node-types <types>', 'Comma-separated node types to use (schema only; omit for all business nodes)')
|
|
68
|
+
.option('--json <json>', 'Inline JSON request (validate, create or update only)')
|
|
69
|
+
.option('--args <json>', 'Workflow input as a JSON object (start only; default: {})')
|
|
70
|
+
.option('--url <url>', 'Bridge HTTP(S) URL (or TAPICKER_BRIDGE_URL)', { default: defaultBridgeUrl })
|
|
60
71
|
.option('--token <token>', 'Bridge Bearer token')
|
|
61
72
|
.option('--client-id <clientId>', 'Target Extension clientId when multiple Extensions are connected')
|
|
62
|
-
.option('--
|
|
63
|
-
.option('--watch', 'After start, stream status updates until the Workflow reaches a terminal state')
|
|
73
|
+
.option('--watch', 'Wait for status updates until stopped (start or status only)')
|
|
64
74
|
.option('--timeout <milliseconds>', 'Maximum watch duration; does not stop the Workflow')
|
|
65
|
-
.
|
|
75
|
+
.option('--levels <levels>', 'Comma-separated exact log levels (logs only)')
|
|
76
|
+
.option('--limit <count>', 'Number of matching logs, 1–500 (default: 100)')
|
|
77
|
+
.option('--before <id>', 'Read logs before this ID (exclusive)')
|
|
78
|
+
.option('--after <id>', 'Read logs after this ID (exclusive)')
|
|
79
|
+
.option('--run-at <timestamp>', 'Require the saved runAt to match (status, logs, or log)')
|
|
80
|
+
.option('--include-data-summary', 'Include saved data changes (status without --watch only)')
|
|
81
|
+
.action(async (action, workflowId, logId, options) => {
|
|
82
|
+
validateActionOptions(action, options, workflowCommonOptions, workflowActionOptions);
|
|
83
|
+
if (options.timeout !== undefined && !options.watch) {
|
|
84
|
+
throw new BridgeError('InvalidArguments', '--timeout requires --watch', { status: 400 });
|
|
85
|
+
}
|
|
86
|
+
if (action === 'guide' ||
|
|
87
|
+
action === 'schema' ||
|
|
88
|
+
action === 'validate' ||
|
|
89
|
+
action === 'list' ||
|
|
90
|
+
action === 'get' ||
|
|
91
|
+
action === 'create' ||
|
|
92
|
+
action === 'update') {
|
|
93
|
+
// cac 忽略单独的 -;恢复约定的 stdin 标记,不影响其他动作的位置参数。
|
|
94
|
+
if ((action === 'validate' || action === 'create' || action === 'update') &&
|
|
95
|
+
workflowId === undefined &&
|
|
96
|
+
process.argv.includes('-'))
|
|
97
|
+
workflowId = '-';
|
|
98
|
+
await queryWorkflowAuthoring(action, workflowId, logId, options);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!workflowId) {
|
|
102
|
+
throw new BridgeError('InvalidArguments', 'workflowId is required for this action', { status: 400 });
|
|
103
|
+
}
|
|
104
|
+
if (action === 'logs' || action === 'log') {
|
|
105
|
+
await queryWorkflowLogs(action, workflowId, logId, options);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (logId !== undefined)
|
|
109
|
+
throw new BridgeError('InvalidArguments', 'logId is only valid for workflow log', { status: 400 });
|
|
66
110
|
if (action === 'start') {
|
|
67
111
|
await startWorkflow(workflowId, options);
|
|
68
112
|
return;
|
|
69
113
|
}
|
|
70
|
-
if (action === '
|
|
71
|
-
|
|
114
|
+
if (action === 'status' && options.watch) {
|
|
115
|
+
if (options.runAt !== undefined || options.includeDataSummary !== undefined) {
|
|
116
|
+
throw new BridgeError('InvalidArguments', '--run-at and --include-data-summary require a single status query', { status: 400 });
|
|
117
|
+
}
|
|
118
|
+
await watchWorkflowStatus(workflowId, options);
|
|
72
119
|
return;
|
|
73
120
|
}
|
|
74
|
-
if (action === '
|
|
75
|
-
await
|
|
121
|
+
if (action === 'pause' || action === 'resume' || action === 'stop' || action === 'status') {
|
|
122
|
+
await controlWorkflow(action, workflowId, options);
|
|
76
123
|
return;
|
|
77
124
|
}
|
|
78
125
|
throw new BridgeError('CommandNotFound', `Unsupported workflow action: ${action}`, { status: 404 });
|
|
79
126
|
});
|
|
80
127
|
cli
|
|
81
|
-
.command('data <action>
|
|
82
|
-
.option('--
|
|
83
|
-
.option('--port <port>', 'Bridge port', { default: String(DEFAULT_PORT) })
|
|
128
|
+
.command('data <action> [tableId]', 'List, preview, read, export, or delete saved data tables')
|
|
129
|
+
.option('--url <url>', 'Bridge HTTP(S) URL (or TAPICKER_BRIDGE_URL)', { default: defaultBridgeUrl })
|
|
84
130
|
.option('--token <token>', 'Bridge Bearer token')
|
|
85
131
|
.option('--client-id <clientId>', 'Target Extension clientId when multiple Extensions are connected')
|
|
86
|
-
.option('--
|
|
132
|
+
.option('--sample <sample>', 'Oldest or latest rows (preview only; default: oldest)')
|
|
133
|
+
.option('--all', 'Read the full table (get only)')
|
|
134
|
+
.option('--format <format>', 'File format (export only; default: csv)')
|
|
135
|
+
.option('--output <path>', 'Destination file or existing directory (export only)')
|
|
136
|
+
.option('--table-name <name>', 'Filter by table name (list only)')
|
|
137
|
+
.option('--workflow-id <workflowId>', 'Filter by owning Workflow ID (list only)')
|
|
138
|
+
.option('--page <page>', 'Page number (list or get; default: 1)')
|
|
139
|
+
.option('--page-size <count>', 'Page size (list or get)')
|
|
87
140
|
.action(async (action, tableId, options) => {
|
|
88
|
-
|
|
89
|
-
|
|
141
|
+
validateActionOptions(action, options, dataCommonOptions, dataActionOptions);
|
|
142
|
+
if (action === 'export') {
|
|
143
|
+
await exportDataTable(tableId, options);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (action === 'list' && tableId !== undefined) {
|
|
147
|
+
throw new BridgeError('InvalidArguments', 'data list accepts filters as options', { status: 400 });
|
|
90
148
|
}
|
|
91
|
-
const
|
|
149
|
+
const pagination = {
|
|
150
|
+
page: options.page === undefined ? undefined : Number(options.page),
|
|
151
|
+
pageSize: options.pageSize === undefined ? undefined : Number(options.pageSize),
|
|
152
|
+
};
|
|
153
|
+
const parsed = action === 'list'
|
|
154
|
+
? DataListInputSchema.safeParse({ ...pagination, tableName: options.tableName, workflowId: options.workflowId })
|
|
155
|
+
: action === 'preview'
|
|
156
|
+
? DataPreviewInputSchema.safeParse({ tableId, sample: options.sample })
|
|
157
|
+
: action === 'get'
|
|
158
|
+
? DataGetInputSchema.safeParse({ ...pagination, tableId, all: options.all })
|
|
159
|
+
: DataInputSchema.safeParse({ tableId });
|
|
92
160
|
if (!parsed.success) {
|
|
93
161
|
throw new BridgeError('InvalidArguments', `Invalid data ${action} arguments`, {
|
|
94
162
|
status: 400,
|
|
95
|
-
details: parsed.error.issues,
|
|
163
|
+
details: { issues: parsed.error.issues },
|
|
96
164
|
});
|
|
97
165
|
}
|
|
98
|
-
await
|
|
166
|
+
await printBridgeCommandResult(`data.${action}`, parsed.data, options);
|
|
167
|
+
});
|
|
168
|
+
cli
|
|
169
|
+
.command('user <action>', 'Read the current Tapicker account in an Extension')
|
|
170
|
+
.option('--url <url>', 'Bridge HTTP(S) URL (or TAPICKER_BRIDGE_URL)', { default: defaultBridgeUrl })
|
|
171
|
+
.option('--token <token>', 'Bridge Bearer token')
|
|
172
|
+
.option('--client-id <clientId>', 'Target Extension clientId when multiple Extensions are connected')
|
|
173
|
+
.example('tapicker user me')
|
|
174
|
+
.action(async (action, options) => {
|
|
175
|
+
validateActionOptions(action, options, workflowCommonOptions, { me: [] });
|
|
176
|
+
if (cli.args.length > 1) {
|
|
177
|
+
throw new BridgeError('InvalidArguments', 'user me does not take positional arguments', { status: 400 });
|
|
178
|
+
}
|
|
179
|
+
await printBridgeCommandResult('user.me', {}, options);
|
|
99
180
|
});
|
|
100
181
|
cli
|
|
101
182
|
.command('mcp', 'Expose a running Tapicker Bridge to an MCP client over stdio')
|
|
102
183
|
.option('--url <url>', 'Bridge HTTP(S) URL', {
|
|
103
|
-
default:
|
|
184
|
+
default: defaultBridgeUrl,
|
|
104
185
|
})
|
|
105
186
|
.option('--token <token>', 'Bridge Bearer token; prefer TAPICKER_BRIDGE_TOKEN for MCP host configuration')
|
|
106
187
|
.example('tapicker mcp')
|
|
107
188
|
.example('tapicker mcp --url https://bridge.example.com --token <token>')
|
|
108
189
|
.action((options) => {
|
|
109
|
-
const token = options.token
|
|
190
|
+
const token = options.token ?? process.env.TAPICKER_BRIDGE_TOKEN;
|
|
110
191
|
const api = createHttpMcpApi(options.url, token);
|
|
111
192
|
serveStdio(() => createTapickerMcpServer(api), {
|
|
112
193
|
onerror: (error) => console.error('Tapicker MCP stdio error:', error),
|
|
@@ -114,6 +195,9 @@ cli
|
|
|
114
195
|
});
|
|
115
196
|
cli.help((sections) => formatCliHelp(sections));
|
|
116
197
|
cli.version(VERSION);
|
|
198
|
+
// 统一在命令输出前处理更新提示,包括 CAC 默认会在 parse 中直接输出的帮助和版本。
|
|
199
|
+
cli.showHelpOnExit = false;
|
|
200
|
+
cli.showVersionOnExit = false;
|
|
117
201
|
const bridgeActions = {
|
|
118
202
|
start: {
|
|
119
203
|
summary: 'Start the Bridge server in the foreground or as a daemon.',
|
|
@@ -137,82 +221,177 @@ const bridgeActions = {
|
|
|
137
221
|
},
|
|
138
222
|
};
|
|
139
223
|
const workflowActions = {
|
|
224
|
+
guide: {
|
|
225
|
+
summary: 'Read Workflow authoring documentation by topic.',
|
|
226
|
+
description: 'Read overview by default. Follow references.topic with another guide call; reuse the returned clientId.',
|
|
227
|
+
example: 'tapicker workflow guide [topic] [--client-id <uuid>]',
|
|
228
|
+
},
|
|
229
|
+
schema: {
|
|
230
|
+
summary: 'Get a self-contained Workflow document JSON Schema.',
|
|
231
|
+
description: 'Get the schema for the node types you plan to use, as comma-separated names. Omit --node-types for all business nodes.',
|
|
232
|
+
example: 'tapicker workflow schema [--node-types NodeLoop,NodeExtractData]',
|
|
233
|
+
},
|
|
234
|
+
validate: {
|
|
235
|
+
summary: 'Validate a Workflow document.',
|
|
236
|
+
description: 'Read a tapicker/nodes or tapicker/workflow document from --json, a file or stdin (-). Inspect isValid and normalized nodes or issues; does not save or run.',
|
|
237
|
+
example: 'tapicker workflow validate <file|-|--json JSON>',
|
|
238
|
+
},
|
|
239
|
+
list: {
|
|
240
|
+
summary: 'List your Workflow summaries with pagination.',
|
|
241
|
+
description: 'List own Workflows by default, or select shared/pinned. Filter by name or exact host. Does not return nodes.',
|
|
242
|
+
example: 'tapicker workflow list --name example --scope own',
|
|
243
|
+
},
|
|
244
|
+
get: {
|
|
245
|
+
summary: 'Read current Workflow nodes and metadata.',
|
|
246
|
+
description: 'Read the latest Workflow metadata and startup arguments using an ID from workflow list. Nodes are returned with view, edit or manage permission.',
|
|
247
|
+
example: 'tapicker workflow get <workflowId>',
|
|
248
|
+
},
|
|
249
|
+
create: {
|
|
250
|
+
summary: 'Validate and create a private Workflow from JSON.',
|
|
251
|
+
description: 'Read {name, intro?, href?, nodes} from --json, a file or stdin (-). Does not run. Check the list before retrying after timeout or connection failure.',
|
|
252
|
+
example: 'tapicker workflow create <file|-|--json JSON>',
|
|
253
|
+
},
|
|
254
|
+
update: {
|
|
255
|
+
summary: 'Update a Workflow using its last-read revision.',
|
|
256
|
+
description: 'Read {workflowId, revision, name?, intro?, href?, nodes?} from --json, a file or stdin (-). Provide at least one change. On WorkflowConflict, read the latest Workflow and reconcile changes before submitting again.',
|
|
257
|
+
example: 'tapicker workflow update <file|-|--json JSON>',
|
|
258
|
+
},
|
|
140
259
|
start: {
|
|
141
260
|
summary: 'Start one Workflow instance; optionally stream its status with --watch.',
|
|
142
|
-
description: 'Start
|
|
143
|
-
example: `tapicker workflow start <workflowId> [--args '{"name":"Tom"}'] [--
|
|
261
|
+
description: 'Start a saved Workflow from the browser’s active tab and return an accepted receipt with workflowId, workflowName, and clientId. A Workflow supports one active instance at a time. By default this returns after the start response; add --watch to stream status until it completes, stops, or fails.',
|
|
262
|
+
example: `tapicker workflow start <workflowId> [--args '{"name":"Tom"}'] [--watch]`,
|
|
144
263
|
},
|
|
145
264
|
pause: {
|
|
146
265
|
summary: 'Pause the active instance of a Workflow.',
|
|
147
|
-
description: 'Pause the active instance
|
|
266
|
+
description: 'Pause the active instance at its next pause point. An already paused instance stays paused.',
|
|
148
267
|
example: 'tapicker workflow pause <workflowId>',
|
|
149
268
|
},
|
|
150
269
|
resume: {
|
|
151
270
|
summary: 'Resume a paused Workflow instance.',
|
|
152
|
-
description: 'Resume
|
|
271
|
+
description: 'Resume the same paused instance. An already running instance continues.',
|
|
153
272
|
example: 'tapicker workflow resume <workflowId>',
|
|
154
273
|
},
|
|
155
274
|
stop: {
|
|
156
|
-
summary: '
|
|
157
|
-
description: '
|
|
275
|
+
summary: 'Request a stop for the active Workflow instance.',
|
|
276
|
+
description: 'Request a stop for the active instance, including a paused instance. Use workflow status to confirm it has stopped.',
|
|
158
277
|
example: 'tapicker workflow stop <workflowId>',
|
|
159
278
|
},
|
|
160
279
|
status: {
|
|
161
|
-
summary: '
|
|
162
|
-
description: '
|
|
163
|
-
example: 'tapicker workflow status <workflowId>',
|
|
280
|
+
summary: 'Query Workflow status; optionally wait for updates with --watch.',
|
|
281
|
+
description: 'Read the latest recorded status, or add --watch to wait until stopped, timeout, or cancellation. Does not start the Workflow. An already stopped snapshot returns immediately.',
|
|
282
|
+
example: 'tapicker workflow status <workflowId> [--watch] [--timeout <milliseconds>]',
|
|
283
|
+
},
|
|
284
|
+
logs: {
|
|
285
|
+
summary: 'Query latest-run log previews with optional level filters and pagination.',
|
|
286
|
+
description: 'Without a cursor, return the newest matching page in ascending ID order. Use before for older logs, after for new logs, and log for full details. run-at optionally checks the run identity.',
|
|
287
|
+
example: 'tapicker workflow logs <workflowId> [--levels warn,error] [--limit 100] [--before <id> | --after <id>]',
|
|
164
288
|
},
|
|
165
|
-
|
|
166
|
-
summary: '
|
|
167
|
-
description: '
|
|
168
|
-
example: 'tapicker workflow
|
|
289
|
+
log: {
|
|
290
|
+
summary: 'Read one latest-run log with full details.',
|
|
291
|
+
description: 'Return one log by workflowId and logId without preview truncation. run-at optionally checks the run identity.',
|
|
292
|
+
example: 'tapicker workflow log <workflowId> <logId> [--run-at <timestamp>]',
|
|
169
293
|
},
|
|
170
294
|
};
|
|
171
295
|
const dataActions = {
|
|
296
|
+
list: {
|
|
297
|
+
summary: 'List saved data tables.',
|
|
298
|
+
description: 'Filter by table name or owning Workflow ID. Returns table metadata without using export quota.',
|
|
299
|
+
example: 'tapicker data list --table-name products --page 1',
|
|
300
|
+
},
|
|
301
|
+
preview: {
|
|
302
|
+
summary: 'Preview up to 10 rows without using export quota.',
|
|
303
|
+
description: 'Inspect the oldest or latest inserted rows. A size-limited response can contain fewer rows.',
|
|
304
|
+
example: 'tapicker data preview <tableId> --sample latest',
|
|
305
|
+
},
|
|
172
306
|
get: {
|
|
173
|
-
summary: '
|
|
174
|
-
description: '
|
|
307
|
+
summary: 'Read a page or all rows from an extracted data table.',
|
|
308
|
+
description: 'Read page 1 with 50 rows by default, or use --all to read the full table. Returned rows consume export quota. If the response is lost, quota may already have been deducted.',
|
|
175
309
|
example: 'tapicker data get <tableId>',
|
|
176
310
|
},
|
|
311
|
+
export: {
|
|
312
|
+
summary: 'Export a saved data table to a local file.',
|
|
313
|
+
description: 'Export the entire table. Returned rows count against export quota. Use --output to choose a new file or an existing directory.',
|
|
314
|
+
example: 'tapicker data export <tableId> --format xlsx --output ./products.xlsx',
|
|
315
|
+
},
|
|
177
316
|
delete: {
|
|
178
|
-
summary: 'Delete
|
|
179
|
-
description: 'Delete the
|
|
317
|
+
summary: 'Delete a saved table and all its rows.',
|
|
318
|
+
description: 'Delete the saved table and all its rows. Use the ID from Copy ID in Tapicker Data.',
|
|
180
319
|
example: 'tapicker data delete <tableId>',
|
|
181
320
|
},
|
|
182
321
|
};
|
|
183
322
|
const workflowCommonOptions = [
|
|
184
|
-
['--
|
|
185
|
-
['--
|
|
186
|
-
['--token <token>', 'Bridge Bearer token'],
|
|
187
|
-
['--client-id <clientId>', 'Select an Extension; required when multiple are ready'],
|
|
188
|
-
['--https', 'Use HTTPS/WSS instead of HTTP/WS'],
|
|
189
|
-
];
|
|
190
|
-
const dataCommonOptions = [
|
|
191
|
-
['--host <host>', `Bridge host (default: ${DEFAULT_HOST})`],
|
|
192
|
-
['--port <port>', `Bridge port (default: ${DEFAULT_PORT})`],
|
|
193
|
-
['--token <token>', 'Bridge Bearer token'],
|
|
323
|
+
['--url <url>', `Bridge HTTP(S) URL (TAPICKER_BRIDGE_URL or http://${DEFAULT_HOST}:${DEFAULT_PORT})`],
|
|
324
|
+
['--token <token>', 'Bridge Bearer token (or TAPICKER_BRIDGE_TOKEN)'],
|
|
194
325
|
['--client-id <clientId>', 'Select an Extension; required when multiple are ready'],
|
|
195
|
-
['--https', 'Use HTTPS instead of HTTP'],
|
|
196
326
|
];
|
|
327
|
+
const dataCommonOptions = workflowCommonOptions;
|
|
328
|
+
const dataActionOptions = {
|
|
329
|
+
list: [
|
|
330
|
+
['--table-name <name>', 'Match a case-insensitive substring of table names'],
|
|
331
|
+
['--workflow-id <workflowId>', 'Match the owning Workflow ID'],
|
|
332
|
+
['--page <page>', 'Page number (default: 1)'],
|
|
333
|
+
['--page-size <count>', 'Tables per page (default: 20, max: 100)'],
|
|
334
|
+
],
|
|
335
|
+
preview: [['--sample <sample>', 'oldest or latest (default: oldest)']],
|
|
336
|
+
get: [
|
|
337
|
+
['--all', 'Read the full table; omit pagination options'],
|
|
338
|
+
['--page <page>', 'Page number (default: 1)'],
|
|
339
|
+
['--page-size <count>', 'Rows per page (default: 50, max: 500)'],
|
|
340
|
+
],
|
|
341
|
+
export: [
|
|
342
|
+
['--format <format>', 'xlsx, xls, csv, json, xml, or txt (default: csv)'],
|
|
343
|
+
['--output <path>', 'Destination file or existing directory (required)'],
|
|
344
|
+
],
|
|
345
|
+
delete: [],
|
|
346
|
+
};
|
|
347
|
+
const workflowJsonOptions = [['--json <json>', 'Inline JSON input; use instead of a file or -']];
|
|
197
348
|
const workflowActionOptions = {
|
|
349
|
+
guide: [],
|
|
350
|
+
schema: [['--node-types <types>', 'Comma-separated node types to use; omit for all business nodes']],
|
|
351
|
+
validate: workflowJsonOptions,
|
|
352
|
+
list: [
|
|
353
|
+
['--scope <scope>', 'own (default), shared or pinned'],
|
|
354
|
+
['--name <name>', 'Match a substring of full names'],
|
|
355
|
+
['--host <host>', 'Match host exactly; omit for all sites'],
|
|
356
|
+
['--page <page>', 'Page number (default: 1)'],
|
|
357
|
+
['--page-size <count>', 'Page size, 1–100 (default: 20)'],
|
|
358
|
+
],
|
|
359
|
+
get: [],
|
|
360
|
+
create: workflowJsonOptions,
|
|
361
|
+
update: workflowJsonOptions,
|
|
198
362
|
start: [
|
|
199
363
|
['--args <json>', 'Workflow input object (default: {})'],
|
|
200
|
-
['--tab <target>', 'Target tab: active (default), new, or id:<positive-tabId>'],
|
|
201
364
|
['--watch', 'Stream status updates after starting, until a terminal state'],
|
|
202
365
|
['--timeout <milliseconds>', 'Limit the --watch duration; does not stop the Workflow'],
|
|
203
366
|
],
|
|
204
367
|
pause: [],
|
|
205
368
|
resume: [],
|
|
206
369
|
stop: [],
|
|
207
|
-
status: [
|
|
208
|
-
|
|
370
|
+
status: [
|
|
371
|
+
['--run-at <timestamp>', 'Require the saved runAt to match (single query only)'],
|
|
372
|
+
['--include-data-summary', 'Include saved data changes (single query only)'],
|
|
373
|
+
['--watch', 'Wait for status updates without starting the Workflow; return immediately if stopped'],
|
|
374
|
+
['--timeout <milliseconds>', 'Limit the --watch duration; does not stop the Workflow'],
|
|
375
|
+
],
|
|
376
|
+
logs: [
|
|
377
|
+
['--levels <levels>', 'Comma-separated exact levels: trace,debug,info,warn,error'],
|
|
378
|
+
['--limit <count>', 'Matching records per page, 1–500 (default: 100)'],
|
|
379
|
+
['--before <id>', 'Read older matching logs before this ID (exclusive)'],
|
|
380
|
+
['--after <id>', 'Read newer matching logs after this ID (exclusive)'],
|
|
381
|
+
['--run-at <timestamp>', 'Require the saved runAt to match'],
|
|
382
|
+
],
|
|
383
|
+
log: [['--run-at <timestamp>', 'Require the saved runAt to match']],
|
|
209
384
|
};
|
|
210
385
|
const workflowGeneralOptions = [
|
|
211
386
|
...workflowCommonOptions,
|
|
387
|
+
...workflowActionOptions.schema,
|
|
388
|
+
...workflowActionOptions.list,
|
|
389
|
+
['--json <json>', 'Inline JSON input (validate, create or update only)'],
|
|
212
390
|
['--args <json>', 'Workflow input object (default: {}; start only)'],
|
|
213
|
-
['--
|
|
214
|
-
['--
|
|
215
|
-
|
|
391
|
+
['--watch', 'Wait for status updates until stopped (start or status only)'],
|
|
392
|
+
['--timeout <milliseconds>', 'Limit --watch duration; does not stop the Workflow'],
|
|
393
|
+
...workflowActionOptions.logs,
|
|
394
|
+
['--include-data-summary', 'Include saved data changes (status without --watch only)'],
|
|
216
395
|
];
|
|
217
396
|
const bridgeCommonOptions = [['--data-dir <path>', 'Bridge state directory (TAPICKER_DATA_DIR or ~/.tapicker)']];
|
|
218
397
|
const bridgeActionOptions = {
|
|
@@ -229,8 +408,32 @@ const bridgeActionOptions = {
|
|
|
229
408
|
code: [['--refresh', 'Rotate the pairing code and unlock pairing after rate limiting']],
|
|
230
409
|
extensions: [],
|
|
231
410
|
};
|
|
411
|
+
/**
|
|
412
|
+
* 按动作限制参数,避免注册在同一 CLI 分组上的选项被静默忽略。
|
|
413
|
+
*/
|
|
414
|
+
function validateActionOptions(action, options, common, actions) {
|
|
415
|
+
if (!Object.hasOwn(actions, action)) {
|
|
416
|
+
throw new BridgeError('CommandNotFound', `Unsupported action: ${action}`, { status: 404 });
|
|
417
|
+
}
|
|
418
|
+
const allowed = new Set([...common, ...actions[action]].map(([flag]) => flag
|
|
419
|
+
.split(' ')[0]
|
|
420
|
+
.slice(2)
|
|
421
|
+
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())));
|
|
422
|
+
for (const [key, value] of Object.entries(options)) {
|
|
423
|
+
if (key === '--' || value === undefined || allowed.has(key))
|
|
424
|
+
continue;
|
|
425
|
+
const flag = key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
426
|
+
throw new BridgeError('InvalidArguments', `--${flag} is not valid for ${action}`, { status: 400 });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
232
429
|
function formatCliHelp(sections) {
|
|
233
430
|
const command = cli.matchedCommandName;
|
|
431
|
+
if (command === 'user') {
|
|
432
|
+
const usage = sections.find((section) => section.title === 'Usage');
|
|
433
|
+
if (usage)
|
|
434
|
+
usage.body = ' $ tapicker user me [options]';
|
|
435
|
+
return sections;
|
|
436
|
+
}
|
|
234
437
|
if (command === 'mcp') {
|
|
235
438
|
const usage = sections.find((section) => section.title === 'Usage');
|
|
236
439
|
if (usage)
|
|
@@ -247,21 +450,28 @@ function formatCliHelp(sections) {
|
|
|
247
450
|
const action = cli.args[0];
|
|
248
451
|
const actionMap = command === 'workflow' ? workflowActions : command === 'bridge' ? bridgeActions : dataActions;
|
|
249
452
|
const actionInfo = actionMap[action];
|
|
250
|
-
const actionOptions = command === 'workflow' ? workflowActionOptions : command === 'bridge' ? bridgeActionOptions :
|
|
453
|
+
const actionOptions = command === 'workflow' ? workflowActionOptions : command === 'bridge' ? bridgeActionOptions : dataActionOptions;
|
|
251
454
|
const commonOptions = command === 'workflow' ? workflowCommonOptions : command === 'bridge' ? bridgeCommonOptions : dataCommonOptions;
|
|
252
455
|
const usage = sections.find((section) => section.title === 'Usage');
|
|
253
456
|
if (usage) {
|
|
254
457
|
if (actionInfo && command === 'workflow') {
|
|
255
|
-
|
|
458
|
+
const target = action === 'guide'
|
|
459
|
+
? ' [topic]'
|
|
460
|
+
: action === 'schema' || action === 'list'
|
|
461
|
+
? ''
|
|
462
|
+
: action === 'validate' || action === 'create' || action === 'update'
|
|
463
|
+
? ' <file|-|--json JSON>'
|
|
464
|
+
: ' <workflowId>';
|
|
465
|
+
usage.body = ` $ tapicker workflow ${action}${target}${action === 'log' ? ' <logId>' : ''} [options]`;
|
|
256
466
|
}
|
|
257
467
|
else if (actionInfo && command === 'data') {
|
|
258
|
-
usage.body = ` $ tapicker data ${action} <tableId> [options]`;
|
|
468
|
+
usage.body = ` $ tapicker data ${action}${action === 'list' ? '' : ' <tableId>'} [options]`;
|
|
259
469
|
}
|
|
260
470
|
else if (actionInfo) {
|
|
261
471
|
usage.body = ` $ tapicker bridge ${action} [options]`;
|
|
262
472
|
}
|
|
263
473
|
else {
|
|
264
|
-
const target = command === 'workflow' ? '
|
|
474
|
+
const target = command === 'workflow' ? ' [target]' : command === 'data' ? ' [tableId]' : '';
|
|
265
475
|
usage.body = ` $ tapicker ${command} <action>${target} [options]`;
|
|
266
476
|
}
|
|
267
477
|
}
|
|
@@ -274,7 +484,15 @@ function formatCliHelp(sections) {
|
|
|
274
484
|
...commonOptions,
|
|
275
485
|
...Object.entries(actionOptions).flatMap(([name, entries]) => entries.map(([option, description]) => [option, `${description} (${name} only)`])),
|
|
276
486
|
]
|
|
277
|
-
:
|
|
487
|
+
: [
|
|
488
|
+
...commonOptions,
|
|
489
|
+
...dataActionOptions.list.slice(0, 2),
|
|
490
|
+
...dataActionOptions.preview,
|
|
491
|
+
dataActionOptions.get[0],
|
|
492
|
+
...dataActionOptions.export,
|
|
493
|
+
['--page <page>', 'Page number (list or get)'],
|
|
494
|
+
['--page-size <count>', 'Page size (list or get)'],
|
|
495
|
+
];
|
|
278
496
|
const optionsSection = sections.find((section) => section.title === 'Options');
|
|
279
497
|
if (optionsSection)
|
|
280
498
|
optionsSection.body = formatOptionRows(options);
|
|
@@ -339,8 +557,12 @@ async function startBridge(options) {
|
|
|
339
557
|
}
|
|
340
558
|
process.exit(0);
|
|
341
559
|
};
|
|
342
|
-
process.once('SIGINT', () =>
|
|
343
|
-
|
|
560
|
+
process.once('SIGINT', () => {
|
|
561
|
+
shutdown();
|
|
562
|
+
});
|
|
563
|
+
process.once('SIGTERM', () => {
|
|
564
|
+
shutdown();
|
|
565
|
+
});
|
|
344
566
|
try {
|
|
345
567
|
controlServer = await startBridgeControlServer(dataDir, async (action) => {
|
|
346
568
|
if (action === 'code.get') {
|
|
@@ -353,7 +575,9 @@ async function startBridge(options) {
|
|
|
353
575
|
if (action === 'extensions.list') {
|
|
354
576
|
return server.bridge.getExtensionStatuses();
|
|
355
577
|
}
|
|
356
|
-
setTimeout(() =>
|
|
578
|
+
setTimeout(() => {
|
|
579
|
+
shutdown();
|
|
580
|
+
}, 50);
|
|
357
581
|
return { pid: process.pid, bridgeId: server.bridgeId };
|
|
358
582
|
});
|
|
359
583
|
if (isDaemonChild) {
|
|
@@ -397,7 +621,7 @@ async function startBridge(options) {
|
|
|
397
621
|
bridgeId: server.bridgeId,
|
|
398
622
|
httpUrl: address.httpUrl,
|
|
399
623
|
wsUrl: address.wsUrl,
|
|
400
|
-
|
|
624
|
+
workflowStatusWatchWsUrl: address.workflowStatusWatchWsUrl,
|
|
401
625
|
dataDir,
|
|
402
626
|
logPath: getBridgeDaemonLogPath(dataDir),
|
|
403
627
|
pairingCode,
|
|
@@ -591,7 +815,7 @@ function isDaemonReadyMessage(value) {
|
|
|
591
815
|
typeof message.bridgeId === 'string' &&
|
|
592
816
|
typeof message.httpUrl === 'string' &&
|
|
593
817
|
typeof message.wsUrl === 'string' &&
|
|
594
|
-
typeof message.
|
|
818
|
+
typeof message.workflowStatusWatchWsUrl === 'string' &&
|
|
595
819
|
typeof message.dataDir === 'string' &&
|
|
596
820
|
typeof message.logPath === 'string' &&
|
|
597
821
|
typeof message.authenticationEnabled === 'boolean');
|
|
@@ -632,16 +856,16 @@ async function isManagedBridgeRunning(state) {
|
|
|
632
856
|
async function startWorkflow(workflowId, options) {
|
|
633
857
|
let args;
|
|
634
858
|
try {
|
|
635
|
-
args = JSON.parse(options.args);
|
|
859
|
+
args = JSON.parse(options.args ?? '{}');
|
|
636
860
|
}
|
|
637
861
|
catch (error) {
|
|
638
862
|
throw new BridgeError('InvalidArguments', '--args must be valid JSON', { status: 400, cause: error });
|
|
639
863
|
}
|
|
640
|
-
const parsed = WorkflowStartInputSchema.safeParse({ workflowId, args
|
|
864
|
+
const parsed = WorkflowStartInputSchema.safeParse({ workflowId, args });
|
|
641
865
|
if (!parsed.success) {
|
|
642
866
|
throw new BridgeError('InvalidArguments', 'Invalid Workflow start arguments', {
|
|
643
867
|
status: 400,
|
|
644
|
-
details: parsed.error.issues,
|
|
868
|
+
details: { issues: parsed.error.issues },
|
|
645
869
|
});
|
|
646
870
|
}
|
|
647
871
|
if (!options.watch) {
|
|
@@ -649,51 +873,60 @@ async function startWorkflow(workflowId, options) {
|
|
|
649
873
|
return;
|
|
650
874
|
}
|
|
651
875
|
const timeoutMs = parseWatchTimeout(options.timeout);
|
|
652
|
-
const connection = await
|
|
876
|
+
const connection = await openWorkflowStatusWatch(parsed.data.workflowId, options);
|
|
653
877
|
try {
|
|
654
|
-
const
|
|
878
|
+
const receipt = await requestBridgeCommand('workflow.start', parsed.data, {
|
|
655
879
|
...options,
|
|
656
880
|
clientId: connection.clientId,
|
|
657
881
|
});
|
|
658
|
-
|
|
882
|
+
const startResult = WorkflowStartResultSchema.safeParse(receipt);
|
|
883
|
+
if (!startResult.success ||
|
|
884
|
+
startResult.data.workflowId !== parsed.data.workflowId ||
|
|
885
|
+
startResult.data.clientId !== connection.clientId) {
|
|
886
|
+
throw new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid Workflow start receipt', {
|
|
887
|
+
status: 502,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
const { runAt } = startResult.data;
|
|
891
|
+
console.log(JSON.stringify({ type: 'workflow.start', data: startResult.data }));
|
|
659
892
|
const result = await waitForWorkflowStatus({
|
|
660
893
|
clientId: connection.clientId,
|
|
661
|
-
workflowId,
|
|
662
|
-
|
|
663
|
-
|
|
894
|
+
workflowId: parsed.data.workflowId,
|
|
895
|
+
runAt,
|
|
896
|
+
subscribe: (onStatus, onClose) => subscribeToWorkflowStatusWatch(connection, onStatus, onClose),
|
|
897
|
+
readStatus: () => readWorkflowStatus(parsed.data.workflowId, connection.clientId, options, runAt),
|
|
664
898
|
onStatus: printWorkflowStatus,
|
|
665
899
|
timeoutMs,
|
|
666
900
|
});
|
|
667
|
-
|
|
901
|
+
printWatchResult(result);
|
|
668
902
|
}
|
|
669
903
|
finally {
|
|
670
904
|
connection.close();
|
|
671
905
|
}
|
|
672
906
|
}
|
|
673
|
-
async function
|
|
907
|
+
async function watchWorkflowStatus(workflowId, options) {
|
|
674
908
|
const timeoutMs = parseWatchTimeout(options.timeout);
|
|
675
|
-
const connection = await
|
|
909
|
+
const connection = await openWorkflowStatusWatch(workflowId, options);
|
|
676
910
|
try {
|
|
677
911
|
const result = await waitForWorkflowStatus({
|
|
678
912
|
clientId: connection.clientId,
|
|
679
913
|
workflowId,
|
|
680
|
-
subscribe: (onStatus, onClose) =>
|
|
914
|
+
subscribe: (onStatus, onClose) => subscribeToWorkflowStatusWatch(connection, onStatus, onClose),
|
|
681
915
|
readStatus: () => readWorkflowStatus(workflowId, connection.clientId, options),
|
|
682
916
|
onStatus: printWorkflowStatus,
|
|
683
917
|
timeoutMs,
|
|
684
918
|
});
|
|
685
|
-
|
|
919
|
+
printWatchResult(result);
|
|
686
920
|
}
|
|
687
921
|
finally {
|
|
688
922
|
connection.close();
|
|
689
923
|
}
|
|
690
924
|
}
|
|
691
|
-
async function
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
return connectWorkflowWatch(workflowWatchWebSocketUrl(bridgeUrl), { clientId: options.clientId, workflowId }, options.token);
|
|
925
|
+
async function openWorkflowStatusWatch(workflowId, options) {
|
|
926
|
+
const bridgeUrl = parseBridgeUrl(options.url);
|
|
927
|
+
return connectWorkflowStatusWatch(workflowStatusWatchWebSocketUrl(bridgeUrl), { clientId: options.clientId, workflowId }, options.token ?? process.env.TAPICKER_BRIDGE_TOKEN);
|
|
695
928
|
}
|
|
696
|
-
function
|
|
929
|
+
function subscribeToWorkflowStatusWatch(connection, onStatus, onClose) {
|
|
697
930
|
const removeStatusListener = connection.onStatus(onStatus);
|
|
698
931
|
const removeCloseListener = connection.onClose(onClose);
|
|
699
932
|
return () => {
|
|
@@ -701,15 +934,15 @@ function subscribeToWorkflowWatch(connection, onStatus, onClose) {
|
|
|
701
934
|
removeCloseListener();
|
|
702
935
|
};
|
|
703
936
|
}
|
|
704
|
-
function
|
|
705
|
-
|
|
706
|
-
console.log(JSON.stringify({ type: 'workflow.watch:res', data: { ok: true, data: result } }));
|
|
707
|
-
}
|
|
937
|
+
function printWatchResult(result) {
|
|
938
|
+
console.log(JSON.stringify({ type: 'workflow.status.watch', data: result }));
|
|
708
939
|
}
|
|
709
|
-
async function readWorkflowStatus(workflowId, clientId, options) {
|
|
710
|
-
const result = await requestBridgeCommand('workflow.status', { workflowId }, { ...options, clientId });
|
|
711
|
-
const parsed = WorkflowStatusEventSchema.safeParse({ ...result, clientId });
|
|
712
|
-
if (!parsed.success ||
|
|
940
|
+
async function readWorkflowStatus(workflowId, clientId, options, runAt) {
|
|
941
|
+
const result = await requestBridgeCommand('workflow.status', { workflowId, runAt }, { ...options, clientId });
|
|
942
|
+
const parsed = WorkflowStatusEventSchema.strip().safeParse({ ...result, clientId });
|
|
943
|
+
if (!parsed.success ||
|
|
944
|
+
parsed.data.workflowId !== workflowId ||
|
|
945
|
+
(runAt !== undefined && parsed.data.runAt !== runAt)) {
|
|
713
946
|
throw new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid Workflow status response', {
|
|
714
947
|
status: 502,
|
|
715
948
|
details: parsed.success ? undefined : parsed.error.issues,
|
|
@@ -718,7 +951,7 @@ async function readWorkflowStatus(workflowId, clientId, options) {
|
|
|
718
951
|
return parsed.data;
|
|
719
952
|
}
|
|
720
953
|
function printWorkflowStatus(status) {
|
|
721
|
-
console.log(JSON.stringify({ type: 'workflow.status', data: status }));
|
|
954
|
+
console.log(JSON.stringify({ type: 'workflow.status.changed', data: status }));
|
|
722
955
|
}
|
|
723
956
|
function parseWatchTimeout(value) {
|
|
724
957
|
if (value === undefined)
|
|
@@ -731,28 +964,129 @@ function parseWatchTimeout(value) {
|
|
|
731
964
|
}
|
|
732
965
|
return timeoutMs;
|
|
733
966
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
967
|
+
/**
|
|
968
|
+
* 编写命令复用既有 HTTP 协议,只在 CLI 层读取片段文件和转换参数。
|
|
969
|
+
*/
|
|
970
|
+
async function queryWorkflowAuthoring(action, target, extra, options) {
|
|
971
|
+
if (extra !== undefined || ((action === 'schema' || action === 'list') && target !== undefined)) {
|
|
972
|
+
throw new BridgeError('InvalidArguments', `Unexpected positional argument for workflow ${action}`, { status: 400 });
|
|
973
|
+
}
|
|
974
|
+
if ([options.levels, options.limit, options.before, options.after, options.runAt].some((value) => value !== undefined)) {
|
|
975
|
+
throw new BridgeError('InvalidArguments', 'Log query options are not valid for authoring commands', { status: 400 });
|
|
976
|
+
}
|
|
977
|
+
let input;
|
|
978
|
+
if (action === 'guide')
|
|
979
|
+
input = { topic: target };
|
|
980
|
+
else if (action === 'schema') {
|
|
981
|
+
if (options.nodeTypes !== undefined && typeof options.nodeTypes !== 'string') {
|
|
982
|
+
throw new BridgeError('InvalidArguments', 'Provide --node-types once with comma-separated node type names', {
|
|
983
|
+
status: 400,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
input = { nodeTypes: options.nodeTypes?.split(',').map((type) => type.trim()) };
|
|
987
|
+
}
|
|
988
|
+
else if (action === 'list')
|
|
989
|
+
input = {
|
|
990
|
+
scope: options.scope,
|
|
991
|
+
name: options.name,
|
|
992
|
+
host: options.host,
|
|
993
|
+
page: options.page === undefined ? undefined : Number(options.page),
|
|
994
|
+
pageSize: options.pageSize === undefined ? undefined : Number(options.pageSize),
|
|
995
|
+
};
|
|
996
|
+
else if (action === 'get')
|
|
997
|
+
input = { workflowId: target };
|
|
998
|
+
else {
|
|
999
|
+
const inline = options.json;
|
|
1000
|
+
if (inline !== undefined && (typeof inline !== 'string' || target !== undefined)) {
|
|
1001
|
+
throw new BridgeError('InvalidArguments', 'Use --json once, without a file path or -', { status: 400 });
|
|
1002
|
+
}
|
|
1003
|
+
if (inline === undefined && !target) {
|
|
1004
|
+
throw new BridgeError('InvalidArguments', 'Provide --json, a JSON file path, or - for stdin', { status: 400 });
|
|
1005
|
+
}
|
|
1006
|
+
try {
|
|
1007
|
+
const value = JSON.parse(inline ?? readFileSync(target === '-' ? 0 : target, 'utf8'));
|
|
1008
|
+
input = action === 'validate' ? { fragment: value } : value;
|
|
1009
|
+
}
|
|
1010
|
+
catch (cause) {
|
|
1011
|
+
throw new BridgeError('InvalidArguments', inline === undefined ? 'Unable to read Workflow JSON' : '--json must be valid JSON', {
|
|
1012
|
+
status: 400,
|
|
1013
|
+
cause,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
737
1016
|
}
|
|
738
|
-
|
|
739
|
-
|
|
1017
|
+
const schema = action === 'guide'
|
|
1018
|
+
? WorkflowGuideInputSchema
|
|
1019
|
+
: action === 'schema'
|
|
1020
|
+
? WorkflowSchemaInputSchema
|
|
1021
|
+
: action === 'validate'
|
|
1022
|
+
? WorkflowValidateInputSchema
|
|
1023
|
+
: action === 'list'
|
|
1024
|
+
? WorkflowListInputSchema
|
|
1025
|
+
: action === 'get'
|
|
1026
|
+
? WorkflowGetInputSchema
|
|
1027
|
+
: action === 'create'
|
|
1028
|
+
? WorkflowCreateInputSchema
|
|
1029
|
+
: WorkflowUpdateInputSchema;
|
|
1030
|
+
const parsed = schema.safeParse(input);
|
|
1031
|
+
if (!parsed.success) {
|
|
1032
|
+
throw new BridgeError('InvalidArguments', `Invalid workflow.${action} arguments`, {
|
|
1033
|
+
status: 400,
|
|
1034
|
+
details: { issues: parsed.error.issues },
|
|
1035
|
+
});
|
|
740
1036
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1037
|
+
await sendWorkflowCommand(`workflow.${action}`, parsed.data, options);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* 将命令行参数转换为统一日志查询协议,发出请求前校验分页约束。
|
|
1041
|
+
*/
|
|
1042
|
+
async function queryWorkflowLogs(action, workflowId, logId, options) {
|
|
1043
|
+
if (action === 'logs' && logId !== undefined)
|
|
1044
|
+
throw new BridgeError('InvalidArguments', 'logId is only valid for workflow log', { status: 400 });
|
|
1045
|
+
if (action === 'log' &&
|
|
1046
|
+
[options.levels, options.limit, options.before, options.after].some((value) => value !== undefined)) {
|
|
1047
|
+
throw new BridgeError('InvalidArguments', 'Log filters and pagination are only valid for workflow logs', {
|
|
745
1048
|
status: 400,
|
|
746
1049
|
});
|
|
747
1050
|
}
|
|
748
|
-
|
|
1051
|
+
if (options.levels !== undefined && typeof options.levels !== 'string') {
|
|
1052
|
+
throw new BridgeError('InvalidArguments', 'Provide --levels once with comma-separated levels, such as --levels warn,error', {
|
|
1053
|
+
status: 400,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
const input = action === 'log'
|
|
1057
|
+
? {
|
|
1058
|
+
workflowId,
|
|
1059
|
+
logId: Number(logId),
|
|
1060
|
+
runAt: options.runAt,
|
|
1061
|
+
}
|
|
1062
|
+
: {
|
|
1063
|
+
workflowId,
|
|
1064
|
+
runAt: options.runAt,
|
|
1065
|
+
levels: options.levels?.split(',').map((level) => level.trim()),
|
|
1066
|
+
limit: options.limit === undefined ? undefined : Number(options.limit),
|
|
1067
|
+
before: options.before === undefined ? undefined : Number(options.before),
|
|
1068
|
+
after: options.after === undefined ? undefined : Number(options.after),
|
|
1069
|
+
};
|
|
1070
|
+
const parsed = (action === 'log' ? WorkflowLogInputSchema : WorkflowLogsInputSchema).safeParse(input);
|
|
1071
|
+
if (!parsed.success)
|
|
1072
|
+
throw new BridgeError('InvalidArguments', `Invalid workflow.${action} arguments`, {
|
|
1073
|
+
status: 400,
|
|
1074
|
+
details: { issues: parsed.error.issues },
|
|
1075
|
+
});
|
|
1076
|
+
await sendWorkflowCommand(`workflow.${action}`, parsed.data, options);
|
|
749
1077
|
}
|
|
750
1078
|
async function controlWorkflow(action, workflowId, options) {
|
|
751
|
-
const parsed =
|
|
1079
|
+
const parsed = action === 'status'
|
|
1080
|
+
? WorkflowStatusInputSchema.safeParse({
|
|
1081
|
+
workflowId,
|
|
1082
|
+
runAt: options.runAt,
|
|
1083
|
+
includeDataSummary: options.includeDataSummary,
|
|
1084
|
+
})
|
|
1085
|
+
: WorkflowControlInputSchema.safeParse({ workflowId });
|
|
752
1086
|
if (!parsed.success) {
|
|
753
1087
|
throw new BridgeError('InvalidArguments', `Invalid Workflow ${action} arguments`, {
|
|
754
1088
|
status: 400,
|
|
755
|
-
details: parsed.error.issues,
|
|
1089
|
+
details: { issues: parsed.error.issues },
|
|
756
1090
|
});
|
|
757
1091
|
}
|
|
758
1092
|
await sendWorkflowCommand(`workflow.${action}`, parsed.data, options);
|
|
@@ -762,31 +1096,103 @@ async function sendWorkflowCommand(command, input, options) {
|
|
|
762
1096
|
console.log(JSON.stringify(result, null, 2));
|
|
763
1097
|
return result;
|
|
764
1098
|
}
|
|
765
|
-
|
|
1099
|
+
/**
|
|
1100
|
+
* 先验证目标路径,再发起导出;目录模式使用回执文件名并避开重名。
|
|
1101
|
+
*/
|
|
1102
|
+
async function exportDataTable(tableId, options) {
|
|
1103
|
+
const input = DataExportInputSchema.safeParse({ tableId, format: options.format });
|
|
1104
|
+
if (!input.success || typeof options.output !== 'string' || !options.output.trim()) {
|
|
1105
|
+
throw new BridgeError('InvalidArguments', 'Provide a tableId, --output, and a supported --format', { status: 400 });
|
|
1106
|
+
}
|
|
1107
|
+
const output = resolve(options.output);
|
|
1108
|
+
const directory = await stat(output)
|
|
1109
|
+
.then((entry) => entry.isDirectory())
|
|
1110
|
+
.catch((cause) => {
|
|
1111
|
+
if (cause.code === 'ENOENT' && !options.output?.endsWith(sep) && !options.output?.endsWith('/'))
|
|
1112
|
+
return false;
|
|
1113
|
+
throw new BridgeError('ExportDestinationUnavailable', 'Cannot access the destination path', {
|
|
1114
|
+
status: 400,
|
|
1115
|
+
details: { output },
|
|
1116
|
+
cause,
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
if (directory) {
|
|
1120
|
+
await access(output, constants.W_OK).catch((cause) => {
|
|
1121
|
+
throw new BridgeError('ExportDestinationUnavailable', 'Cannot write to the destination directory', {
|
|
1122
|
+
status: 400,
|
|
1123
|
+
details: { output },
|
|
1124
|
+
cause,
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
let target = directory ? undefined : await openExportOutput(output);
|
|
1129
|
+
let saved = false;
|
|
1130
|
+
try {
|
|
1131
|
+
const receipt = DataExportResultSchema.safeParse(await requestBridgeCommand('data.export', input.data, options));
|
|
1132
|
+
if (!receipt.success)
|
|
1133
|
+
throw new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid export receipt', { status: 502 });
|
|
1134
|
+
const token = options.token ?? process.env.TAPICKER_BRIDGE_TOKEN;
|
|
1135
|
+
try {
|
|
1136
|
+
target ??= await openExportOutput(output, receipt.data.filename);
|
|
1137
|
+
const response = await fetch(new URL(receipt.data.downloadPath, parseBridgeUrl(options.url)), {
|
|
1138
|
+
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
|
1139
|
+
redirect: 'error',
|
|
1140
|
+
signal: AbortSignal.timeout(120_000),
|
|
1141
|
+
});
|
|
1142
|
+
if (!response.ok || !response.body)
|
|
1143
|
+
throw new Error(`HTTP ${response.status}`);
|
|
1144
|
+
await pipeline(Readable.fromWeb(response.body), target.file.createWriteStream());
|
|
1145
|
+
}
|
|
1146
|
+
catch (cause) {
|
|
1147
|
+
throw new BridgeError('ExportDownloadFailed', 'Failed to save the export. The file can be downloaded again before it expires.', {
|
|
1148
|
+
status: 502,
|
|
1149
|
+
details: { downloadPath: receipt.data.downloadPath, expiresAt: receipt.data.expiresAt },
|
|
1150
|
+
cause,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
saved = true;
|
|
1154
|
+
console.log(JSON.stringify({
|
|
1155
|
+
...receipt.data,
|
|
1156
|
+
output: target.path,
|
|
1157
|
+
}, null, 2));
|
|
1158
|
+
}
|
|
1159
|
+
finally {
|
|
1160
|
+
if (target) {
|
|
1161
|
+
await target.file.close();
|
|
1162
|
+
if (!saved)
|
|
1163
|
+
await rm(target.path, { force: true });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* 以独占方式创建文件,目录模式在同名时追加编号。
|
|
1169
|
+
*/
|
|
1170
|
+
async function openExportOutput(output, filename) {
|
|
1171
|
+
if (filename !== undefined && (filename === '.' || filename === '..' || /[\\/:\x00-\x1F]/.test(filename))) {
|
|
1172
|
+
throw new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid filename', { status: 502 });
|
|
1173
|
+
}
|
|
1174
|
+
const { name, ext } = parse(filename ?? '');
|
|
1175
|
+
for (let suffix = 0;; suffix++) {
|
|
1176
|
+
const path = filename === undefined ? output : join(output, suffix ? `${name} (${suffix})${ext}` : filename);
|
|
1177
|
+
try {
|
|
1178
|
+
return { path, file: await open(path, 'wx') };
|
|
1179
|
+
}
|
|
1180
|
+
catch (error) {
|
|
1181
|
+
const cause = error;
|
|
1182
|
+
if (cause.code === 'EEXIST' && filename !== undefined)
|
|
1183
|
+
continue;
|
|
1184
|
+
throw new BridgeError(cause.code === 'EEXIST' ? 'ExportDestinationExists' : 'ExportDestinationUnavailable', cause.code === 'EEXIST' ? 'The destination file already exists' : 'Cannot open the destination file', { status: 400, details: { output: path }, cause });
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
async function printBridgeCommandResult(command, input, options) {
|
|
766
1189
|
const result = await requestBridgeCommand(command, input, options);
|
|
767
1190
|
console.log(JSON.stringify(result, null, 2));
|
|
768
1191
|
return result;
|
|
769
1192
|
}
|
|
770
1193
|
async function requestBridgeCommand(command, input, options) {
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
774
|
-
if (options.token) {
|
|
775
|
-
headers.Authorization = `Bearer ${options.token}`;
|
|
776
|
-
}
|
|
777
|
-
const response = await fetch(url, {
|
|
778
|
-
method: 'POST',
|
|
779
|
-
headers,
|
|
780
|
-
body: JSON.stringify({ clientId: options.clientId, input }),
|
|
781
|
-
});
|
|
782
|
-
const body = await readResponseBody(response);
|
|
783
|
-
if (!response.ok) {
|
|
784
|
-
throw new BridgeError(body.error?.code || 'BridgeRequestFailed', body.error?.message || response.statusText, {
|
|
785
|
-
status: response.status,
|
|
786
|
-
details: body.error?.details,
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
return body.data ?? body;
|
|
1194
|
+
const api = createHttpBridgeApi(options.url, options.token ?? process.env.TAPICKER_BRIDGE_TOKEN);
|
|
1195
|
+
return api.executeCommand(command, { clientId: options.clientId, input });
|
|
790
1196
|
}
|
|
791
1197
|
function resolveTls(options) {
|
|
792
1198
|
if (!options.tlsKey && !options.tlsCert) {
|
|
@@ -809,27 +1215,36 @@ function parsePort(value) {
|
|
|
809
1215
|
function isLoopback(host) {
|
|
810
1216
|
return host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
811
1217
|
}
|
|
812
|
-
async function readResponseBody(response) {
|
|
813
|
-
const text = await response.text();
|
|
814
|
-
if (!text) {
|
|
815
|
-
return {};
|
|
816
|
-
}
|
|
817
|
-
try {
|
|
818
|
-
return JSON.parse(text);
|
|
819
|
-
}
|
|
820
|
-
catch {
|
|
821
|
-
return { error: { message: text } };
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
1218
|
async function main() {
|
|
825
1219
|
try {
|
|
826
1220
|
cli.parse(process.argv, { run: false });
|
|
1221
|
+
const showHelp = Boolean(cli.options.help);
|
|
1222
|
+
const showVersion = cli.options.version && (showHelp || cli.matchedCommandName === undefined);
|
|
1223
|
+
const isMcpStdio = cli.matchedCommandName === 'mcp' && !showHelp;
|
|
1224
|
+
if ((cli.matchedCommand || showHelp || showVersion) &&
|
|
1225
|
+
!isMcpStdio &&
|
|
1226
|
+
process.env[DAEMON_CHILD_ENV] !== '1' &&
|
|
1227
|
+
process.env.TAPICKER_NO_UPDATE_NOTIFIER !== '1' &&
|
|
1228
|
+
process.stdin.isTTY &&
|
|
1229
|
+
process.stdout.isTTY &&
|
|
1230
|
+
process.stderr.isTTY) {
|
|
1231
|
+
await showCachedUpdate();
|
|
1232
|
+
}
|
|
1233
|
+
if (showHelp)
|
|
1234
|
+
cli.outputHelp();
|
|
1235
|
+
if (showVersion)
|
|
1236
|
+
cli.outputVersion();
|
|
1237
|
+
if (showHelp || showVersion)
|
|
1238
|
+
return;
|
|
827
1239
|
await cli.runMatchedCommand();
|
|
828
1240
|
}
|
|
829
1241
|
catch (error) {
|
|
830
|
-
|
|
1242
|
+
const failure = error instanceof Error && error.name === 'CACError'
|
|
1243
|
+
? new BridgeError('InvalidArguments', error.message, { status: 400, cause: error })
|
|
1244
|
+
: error;
|
|
1245
|
+
console.error(JSON.stringify({ ok: false, error: serializeBridgeError(failure) }, null, 2));
|
|
831
1246
|
process.exitCode = 1;
|
|
832
1247
|
}
|
|
833
1248
|
}
|
|
834
|
-
|
|
1249
|
+
main();
|
|
835
1250
|
//# sourceMappingURL=cli.js.map
|