@itpay/cli 2.0.38 → 2.0.39
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 +6 -0
- package/dist/src/client/backend.js +3 -0
- package/dist/src/commands/catalog.js +7 -0
- package/dist/src/commands/services.js +92 -1
- package/dist/src/main.js +37 -0
- package/dist/src/state/config.js +2 -2
- package/docs/cli-reference/commands/services/index.md +3 -2
- package/docs/cli-reference/commands/services/run.md +74 -0
- package/docs/cli-reference/index.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,3 +128,9 @@ Source boundaries:
|
|
|
128
128
|
- `src/state`: local Device Authority, idempotency journal, and recovery handles.
|
|
129
129
|
- `docs/cli-reference`: normative command contracts.
|
|
130
130
|
- `docs/agent/buyer`: packaged progressive workflow guidance.
|
|
131
|
+
|
|
132
|
+
### Run a published service
|
|
133
|
+
|
|
134
|
+
Use `itpay services run <service_id> --input-json ./input.json --json` for a published workflow. The input file must contain a JSON object matching the service input schema. With no input file, the CLI reports required fields and a command to continue the same execution.
|
|
135
|
+
|
|
136
|
+
Use `itpay services run <service_id> --execution <execution_id> --json` to resume. At payment, the CLI uses the existing Checkout QR/card handoff. The backend continues after verified payment even if the CLI exits. `--timeout <seconds>` controls initial status polling (default 120, maximum 600). Use existing order/refund commands for refund requests; an application is not a successful refund until its final status confirms success.
|
|
@@ -108,6 +108,9 @@ export class BackendClient {
|
|
|
108
108
|
startServiceExecution(input) {
|
|
109
109
|
return this.http.post("/v1/service-executions", input);
|
|
110
110
|
}
|
|
111
|
+
advanceServiceExecution(id, input, idempotencyKey) {
|
|
112
|
+
return this.http.post(`/v1/service-executions/${encodeURIComponent(id)}/advance`, { input, idempotency_key: idempotencyKey });
|
|
113
|
+
}
|
|
111
114
|
invokeServiceCapability(serviceExecutionID, capabilityID, input) {
|
|
112
115
|
return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/capabilities/${encodeURIComponent(capabilityID)}/invoke`, input, { replaySafe: Boolean(input.idempotency_key) });
|
|
113
116
|
}
|
|
@@ -25,6 +25,7 @@ export async function runCatalogList(backend, options = {}) {
|
|
|
25
25
|
}
|
|
26
26
|
function summarizeService(item) {
|
|
27
27
|
const flow = item.service_flow;
|
|
28
|
+
const offer = item.variants?.[0];
|
|
28
29
|
return {
|
|
29
30
|
service_id: item.service_id ?? null,
|
|
30
31
|
title: item.title,
|
|
@@ -43,6 +44,12 @@ function summarizeService(item) {
|
|
|
43
44
|
description: flow.primary_service.description,
|
|
44
45
|
price: formatProductMoney(flow.primary_service.amount_minor, flow.primary_service.currency),
|
|
45
46
|
},
|
|
47
|
+
} : offer ? {
|
|
48
|
+
primary_offer: {
|
|
49
|
+
title: offer.title || item.title,
|
|
50
|
+
description: item.description ?? "",
|
|
51
|
+
price: formatProductMoney(offer.amount_minor, offer.currency),
|
|
52
|
+
},
|
|
46
53
|
} : {}),
|
|
47
54
|
};
|
|
48
55
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { HttpError } from "../client/http.js";
|
|
2
|
+
import { HttpTransportError } from "../client/transport.js";
|
|
1
3
|
import { operationID } from "../state/config.js";
|
|
2
4
|
import { validateContext } from "../state/client_context.js";
|
|
3
5
|
import { dispatchRender } from "../render/index.js";
|
|
@@ -20,6 +22,10 @@ export async function runServicesStart(backend, serviceID, options = {}) {
|
|
|
20
22
|
...(options.clientContext ?? {}),
|
|
21
23
|
},
|
|
22
24
|
});
|
|
25
|
+
if (response.workflow_entry) {
|
|
26
|
+
writeCommandEnvelope({ status: "input_required", result: { service_execution_id: response.execution.service_execution_id, service_id: serviceID, input_schema: response.workflow_entry.input_schema }, instruction: "根据服务声明填写输入,然后继续同一服务执行。", next: { command: `itpay services run ${serviceID} --execution ${response.execution.service_execution_id} --input-json <file> --json`, reason: "提交买家输入" }, recovery: [] }, { ...options });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
23
29
|
const capability = response.capabilities.find((item) => item.phase === response.execution.phase && !item.requires_payment);
|
|
24
30
|
const requiredInput = requiredInputFields(capability?.input_schema);
|
|
25
31
|
const command = capability
|
|
@@ -384,6 +390,10 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
384
390
|
...(options.deliveryContact ?? {}),
|
|
385
391
|
...(options.email ? { email: options.email } : {}),
|
|
386
392
|
};
|
|
393
|
+
if (!options.resume && !capabilityID) {
|
|
394
|
+
const model = await backend.getServiceExecution(serviceExecutionID);
|
|
395
|
+
capabilityID = model.workflow_entry?.capability_id;
|
|
396
|
+
}
|
|
387
397
|
if (!options.resume && !capabilityID) {
|
|
388
398
|
throw new CommandContractError("capability_required", "--capability is required when creating a service checkout", "使用当前 Service Execution 返回的付费 capability;恢复已有 Checkout 时改用 --resume。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前允许的付费 capability" }]);
|
|
389
399
|
}
|
|
@@ -395,7 +405,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
|
|
|
395
405
|
}
|
|
396
406
|
const lockedInput = options.lockedInput ?? {};
|
|
397
407
|
const missingInput = missingRequiredInput(capability.input_schema, lockedInput);
|
|
398
|
-
if (missingInput.length > 0 && readModel.execution.next_action !== "create_checkout") {
|
|
408
|
+
if (missingInput.length > 0 && !readModel.workflow_entry && readModel.execution.next_action !== "create_checkout") {
|
|
399
409
|
throw new CommandContractError("capability_input_invalid", `missing required capability input: ${missingInput.join(", ")}`, "补齐付费 capability 的 required_input;本次没有创建 quote、Checkout 或订单。", [{ command: checkoutCommand(serviceExecutionID, capability, lockedInput), reason: "提交完整且会被锁定的服务输入" }]);
|
|
400
410
|
}
|
|
401
411
|
if (capability.delivery_email_required && String(deliveryContact.email ?? "").trim() === "") {
|
|
@@ -642,6 +652,32 @@ function servicesNextEnvelope(model) {
|
|
|
642
652
|
const execution = model.execution;
|
|
643
653
|
const currentDelivery = model.current_delivery ?? model.delivery_bindings.at(-1);
|
|
644
654
|
const lockedRefund = model.refunds.find((refund) => refund.access_locked);
|
|
655
|
+
if (model.workflow_entry && !lockedRefund && !["completed", "delivery"].includes(model.workflow?.status ?? "")) {
|
|
656
|
+
const id = execution.service_execution_id;
|
|
657
|
+
const state = model.workflow?.status ?? "input_required";
|
|
658
|
+
const recovery = state === "recovery_required" || state === "failed";
|
|
659
|
+
let command = `itpay services next ${id} --json`;
|
|
660
|
+
if (state === "payment")
|
|
661
|
+
command = `itpay services checkout ${id} --json`;
|
|
662
|
+
if (state === "input_required")
|
|
663
|
+
command = `itpay services run ${execution.service_id} --execution ${id} --input-json <file> --json`;
|
|
664
|
+
return {
|
|
665
|
+
status: state,
|
|
666
|
+
result: {
|
|
667
|
+
service_execution_id: id,
|
|
668
|
+
service_id: execution.service_id,
|
|
669
|
+
workflow: model.workflow,
|
|
670
|
+
...(state === "input_required" ? { input_schema: model.workflow_entry.input_schema } : {}),
|
|
671
|
+
},
|
|
672
|
+
instruction: recovery
|
|
673
|
+
? "执行未完成,请按步骤错误处理;不要重建执行或重复调用。"
|
|
674
|
+
: state === "payment"
|
|
675
|
+
? "服务已到付款步骤,使用现有 Checkout 完成扫码付款。"
|
|
676
|
+
: "继续读取同一执行;缺少输入时按服务声明补齐。",
|
|
677
|
+
next: recovery ? null : { command, reason: "继续当前流程" },
|
|
678
|
+
recovery: [],
|
|
679
|
+
};
|
|
680
|
+
}
|
|
645
681
|
if (lockedRefund) {
|
|
646
682
|
const terminal = lockedRefund.status === "succeeded";
|
|
647
683
|
return {
|
|
@@ -1066,3 +1102,58 @@ function tokenizedCheckoutURL(checkoutURL, displayToken, qrPayload) {
|
|
|
1066
1102
|
return `${checkoutURL}${separator}display_token=${encodeURIComponent(displayToken)}`;
|
|
1067
1103
|
}
|
|
1068
1104
|
}
|
|
1105
|
+
export async function runServicesRun(backend, config, serviceID, input, options = {}) {
|
|
1106
|
+
let id = options.executionID;
|
|
1107
|
+
try {
|
|
1108
|
+
if (!id) {
|
|
1109
|
+
const started = await backend.startServiceExecution({
|
|
1110
|
+
service_id: serviceID,
|
|
1111
|
+
client_context: { host: options.host ?? "terminal", ...(options.target ? { target: options.target } : {}) },
|
|
1112
|
+
});
|
|
1113
|
+
id = started.execution.service_execution_id;
|
|
1114
|
+
if (!started.workflow_entry) {
|
|
1115
|
+
await runServicesNext(backend, id, options);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
let model = await backend.getServiceExecution(id);
|
|
1120
|
+
if (model.execution.service_id !== serviceID)
|
|
1121
|
+
throw new Error("execution belongs to another service");
|
|
1122
|
+
if (!model.workflow_entry || (input === undefined && !model.workflow)) {
|
|
1123
|
+
await runServicesNext(backend, id, options);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (input !== undefined) {
|
|
1127
|
+
model = await backend.advanceServiceExecution(id, input, `workflow-input:${id}`);
|
|
1128
|
+
}
|
|
1129
|
+
const until = Date.now() + (options.timeoutSeconds ?? 120) * 1000;
|
|
1130
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)));
|
|
1131
|
+
while (["queued", "running"].includes(model.workflow?.status ?? "") && Date.now() < until) {
|
|
1132
|
+
await sleep(options.pollIntervalMS ?? 1500);
|
|
1133
|
+
model = await backend.getServiceExecution(id);
|
|
1134
|
+
}
|
|
1135
|
+
if (model.refunds.some(refund => refund.access_locked)) {
|
|
1136
|
+
await runServicesNext(backend, id, options);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (model.workflow?.status === "payment") {
|
|
1140
|
+
await runServicesCheckout(backend, config, id, model.workflow_entry?.capability_id, {
|
|
1141
|
+
...options,
|
|
1142
|
+
...(config.agentType ? { agentType: config.agentType } : {}),
|
|
1143
|
+
resume: model.checkout_bindings.length > 0,
|
|
1144
|
+
});
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
await runServicesNext(backend, id, options);
|
|
1148
|
+
}
|
|
1149
|
+
catch (cause) {
|
|
1150
|
+
if (cause instanceof CommandContractError || cause instanceof HttpError)
|
|
1151
|
+
throw cause;
|
|
1152
|
+
if (cause instanceof HttpTransportError && !id) {
|
|
1153
|
+
throw new CommandContractError("workflow_start_outcome_unknown", cause.message, "创建服务执行时没有收到完整响应。先查询当前身份可见的执行;不要直接重跑并创建替代执行。", [{ command: "itpay services list --json", reason: "查找可能已经创建的服务执行" }]);
|
|
1154
|
+
}
|
|
1155
|
+
if (cause instanceof HttpTransportError)
|
|
1156
|
+
throw cause;
|
|
1157
|
+
throw new CommandContractError("workflow_run_failed", cause instanceof Error ? cause.message : "workflow run failed", "保留当前执行并按错误处理,不要重复创建服务执行。", [{ command: `itpay services run ${serviceID} --execution ${id} --json`, reason: "恢复同一执行" }]);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
package/dist/src/main.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync as readWorkflowInputFile } from "node:fs";
|
|
2
|
+
import { runServicesRun } from "./commands/services.js";
|
|
1
3
|
// V3 CLI entrypoint. Each command maps 1:1 to a route family in
|
|
2
4
|
// services/backend/internal/httpapi/handlers/*.go. Commands only
|
|
3
5
|
// orchestrate; HTTP and rendering live in src/client and src/render.
|
|
@@ -1215,6 +1217,41 @@ vault
|
|
|
1215
1217
|
});
|
|
1216
1218
|
// --- service execution ----------------------------------------------------
|
|
1217
1219
|
const services = program.command("services").description("Generic V3 Service Execution commands");
|
|
1220
|
+
services
|
|
1221
|
+
.command("run")
|
|
1222
|
+
.description("Run a published service and present Checkout when payment is required")
|
|
1223
|
+
.argument("<service_id>")
|
|
1224
|
+
.option("--input-json <file>", "JSON object containing the service input")
|
|
1225
|
+
.option("--execution <execution_id>", "resume the same execution")
|
|
1226
|
+
.option("--timeout <seconds>", "maximum wait before returning current progress", Number, 120)
|
|
1227
|
+
.option("--host <host>")
|
|
1228
|
+
.option("--target <target>")
|
|
1229
|
+
.option("--json", "output JSON")
|
|
1230
|
+
.action(async (serviceID, options) => {
|
|
1231
|
+
const config = loadConfig();
|
|
1232
|
+
try {
|
|
1233
|
+
const input = options.inputJson ? JSON.parse(readWorkflowInputFile(options.inputJson, "utf8")) : undefined;
|
|
1234
|
+
if (input !== undefined && (!input || typeof input !== "object" || Array.isArray(input))) {
|
|
1235
|
+
throw new Error("input JSON must be an object");
|
|
1236
|
+
}
|
|
1237
|
+
if (!Number.isFinite(options.timeout) || options.timeout < 0 || options.timeout > 600) {
|
|
1238
|
+
throw new Error("timeout must be between 0 and 600 seconds");
|
|
1239
|
+
}
|
|
1240
|
+
await runServicesRun(newBackendClient(config), config, serviceID, input, {
|
|
1241
|
+
...(options.execution ? { executionID: options.execution } : {}),
|
|
1242
|
+
jsonOutput: Boolean(options.json),
|
|
1243
|
+
host: withHost(options.host, config.agentType, options.target),
|
|
1244
|
+
...(options.target ? { target: options.target } : {}),
|
|
1245
|
+
timeoutSeconds: options.timeout,
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
reportCLIError(error, {
|
|
1250
|
+
jsonOutput: Boolean(options.json), code: "workflow_run_failed",
|
|
1251
|
+
instruction: "按服务输入声明补齐参数;已有 execution 时继续该执行。", recovery: [],
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1218
1255
|
services
|
|
1219
1256
|
.command("start")
|
|
1220
1257
|
.description("Start a contract-backed service execution")
|
package/dist/src/state/config.js
CHANGED
|
@@ -12,8 +12,8 @@ import { DeviceAuthority } from "./device_authority.js";
|
|
|
12
12
|
import { OperationJournal } from "./operation_journal.js";
|
|
13
13
|
export const DEFAULT_BASE_URL = "https://app.itpay.ai";
|
|
14
14
|
export const SANDBOX_BASE_URL = "https://sandbox.itpay.ai";
|
|
15
|
-
export const CLI_VERSION = "2.0.
|
|
16
|
-
export const API_CONTRACT_REVISION = "sha256:
|
|
15
|
+
export const CLI_VERSION = "2.0.39";
|
|
16
|
+
export const API_CONTRACT_REVISION = "sha256:1d637a77a49d5a821ef55f88bd9107427aede63f9652335b5d1a6a870a7e92a0";
|
|
17
17
|
const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
|
|
18
18
|
const CART_SESSION_FILENAME = "cart.json";
|
|
19
19
|
const OPERATION_JOURNAL_FILENAME = "operations.json";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `itpay services`
|
|
2
2
|
|
|
3
|
-
> **Product boundary:** `itpay` is the single public CLI entry point, and `$itpay` is its user-facing Skill invocation.
|
|
3
|
+
> **Product boundary:** `itpay` is the single public CLI entry point, and `$itpay` is its user-facing Skill invocation. Seller-published workflows enter the same Buyer execution, Checkout, delivery, and refund state machine as ItPay services.
|
|
4
4
|
|
|
5
5
|
## 命令范围
|
|
6
6
|
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
## 子命令
|
|
23
23
|
|
|
24
24
|
- [`start`](start.md)
|
|
25
|
+
- [`run`](run.md)
|
|
25
26
|
- [`invoke`](invoke.md)
|
|
26
27
|
- [`action`](action.md)
|
|
27
28
|
- [`quote`](quote.md)
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
itpay services --help
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
输出十一个子命令及一句选择规则:运行已发布服务使用 `run`,正常推进使用 `next`,深度诊断才使用 `get/events`。未知子命令返回参数错误和本 help,不创建 Execution。
|
|
44
45
|
|
|
45
46
|
## Agent Type / Host
|
|
46
47
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# `itpay services run`
|
|
2
|
+
|
|
3
|
+
## 范围与意义
|
|
4
|
+
|
|
5
|
+
运行 Catalog 中已发布的通用 Buyer workflow。CLI 只提交服务声明要求的输入,并复用现有 Service Execution、Checkout、交付和退款命令;它不解释或执行 Seller 提供的任意命令文本。
|
|
6
|
+
|
|
7
|
+
没有提供输入时,本命令创建一次 Execution,返回已发布的 JSON input schema,并要求 Agent 继续同一 Execution。已有 Execution 必须通过 `--execution` 恢复,不能创建替代 Execution。
|
|
8
|
+
|
|
9
|
+
## 语法与参数
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
itpay services run <service_id>
|
|
13
|
+
[--input-json <file>]
|
|
14
|
+
[--execution <service_execution_id>]
|
|
15
|
+
[--timeout <seconds>]
|
|
16
|
+
[--host <host>]
|
|
17
|
+
[--target <target>]
|
|
18
|
+
[--json]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| 参数 | 说明 |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `service_id` | Catalog 返回的服务 ID。恢复时必须与 Execution 所属服务一致。 |
|
|
24
|
+
| `--input-json` | UTF-8 JSON 文件;顶层必须是 object,并符合服务发布的 input schema。 |
|
|
25
|
+
| `--execution` | 恢复同一 Execution。不得用它切换服务或绕过原状态。 |
|
|
26
|
+
| `--timeout` | 本次等待 workflow 状态变化的秒数,默认 120,范围 0–600;超时只返回当前状态。 |
|
|
27
|
+
| `--host` / `--target` | Host 展示上下文,不是业务输入或 Buyer 身份。 |
|
|
28
|
+
| `--json` | 输出稳定 JSON 信封。 |
|
|
29
|
+
|
|
30
|
+
## 缺少输入
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"status": "input_required",
|
|
35
|
+
"result": {
|
|
36
|
+
"service_execution_id": "<execution_id>",
|
|
37
|
+
"service_id": "<service_id>",
|
|
38
|
+
"input_schema": { "type": "object", "required": ["<field>"] }
|
|
39
|
+
},
|
|
40
|
+
"instruction": "根据服务声明填写输入,然后继续同一服务执行。",
|
|
41
|
+
"next": {
|
|
42
|
+
"command": "itpay services run <service_id> --execution <execution_id> --input-json <file> --json",
|
|
43
|
+
"reason": "提交买家输入"
|
|
44
|
+
},
|
|
45
|
+
"recovery": []
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Agent 必须展示 schema 所需信息并等待用户提供真实输入;不得猜值,也不得为补输入创建新 Execution。
|
|
50
|
+
|
|
51
|
+
## 执行与付款
|
|
52
|
+
|
|
53
|
+
提供输入后,CLI 使用稳定 idempotency key 向同一 Execution 提交一次。`queued` 或 `running` 只轮询该 Execution。进入付款步骤时,CLI 调用现有 `services checkout`,返回其标准 `human_checkout_required` 输出和 Host handoff;不会实现第二套付款逻辑。
|
|
54
|
+
|
|
55
|
+
付款完成后即使 CLI 已退出,Backend 仍继续该 workflow。再次运行:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
itpay services run <service_id> --execution <execution_id> --json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
只恢复当前状态。交付、Vault 授权和退款继续使用 `services next`、`services read-result`、`vault` 与 `refund` 的既有合同。
|
|
62
|
+
|
|
63
|
+
## 终态和异常
|
|
64
|
+
|
|
65
|
+
- 成功、付款、交付、授权、退款锁等状态使用既有命令的标准信封,不额外包装 Seller 专用格式。
|
|
66
|
+
- 既有 Checkout、身份、兼容性和网络错误保持原错误码、instruction 与 recovery;`services run` 不得覆盖可执行的邮箱、登录、升级或状态恢复指令。
|
|
67
|
+
- 如果创建 Execution 后未收到完整响应,返回 `workflow_start_outcome_unknown`,只允许先执行 `itpay services list --json` 查找刚创建的 Execution;不得直接重跑 `services run` 创建替代 Execution。
|
|
68
|
+
- `recovery_required` 或 `failed` 返回当前 workflow step,`next: null`,并明确禁止重建 Execution 或重放结果未知的 Provider 请求。
|
|
69
|
+
- service 不匹配、输入文件不是 JSON object、timeout 越界或 Backend 错误返回 `workflow_run_failed`。
|
|
70
|
+
- `--timeout` 到期不是失败,只返回同一 Execution 当前可恢复状态。
|
|
71
|
+
|
|
72
|
+
## Agent Type / Host
|
|
73
|
+
|
|
74
|
+
所有 Local Agent Type 共享同一状态机和 JSON 事实。Host 只改变付款链接、二维码或附件的展示方式,不改变身份、价格、workflow 或交付权限。
|
|
@@ -78,6 +78,7 @@ Commander 自动提供的 `itpay help [command]` 与 `itpay <group> help [subcom
|
|
|
78
78
|
### 通用服务执行
|
|
79
79
|
|
|
80
80
|
- [`itpay services`](commands/services/index.md)
|
|
81
|
+
- [`itpay services run`](commands/services/run.md)
|
|
81
82
|
- [`itpay services start`](commands/services/start.md)
|
|
82
83
|
- [`itpay services invoke`](commands/services/invoke.md)
|
|
83
84
|
- [`itpay services action`](commands/services/action.md)
|