知识体系——SKILL

最近,继mcp后,skill又大火。

什么是skill

一、CLI+skill代替mcp

背景

MCP带来的痛点:

  1. 配置门槛高:用户需要在 Cursor 里手动配置 MCP Server 的 JSON(路径、参数),换台电脑又得来一遍。

  2. 连接不稳定:MCP Server 通过 stdio 通信,Cursor 偶尔会丢失连接,需要重启。

  3. 工具数量爆炸:当工具超过 40-50 个,AI 的工具选择准确率会下降。MCP 把所有工具一次性暴露给 AI,没有上下文筛选。

  4. 调试黑盒:出了问题很难定位 ------ 是 MCP 协议的问题?是 Server 崩了?是 Cursor 没正确传参?

  5. Ask 模式不可用:Cursor 的 Ask 模式不支持 MCP 工具,只有 Agent/Plan/Debug 模式才能用。

前提

我们注意到:AI 编码助手天生就会执行 shell 命令。无论是 Cursor、Claude Code 还是 Windsurf,它们都有一个强大的 Shell 工具。如果我们把 MCP 工具包装成 CLI 命令,AI 就能直接在终端里执行,不需要任何 MCP 配置。

而要让 cursor 知道 该怎么用这些 CLI 命令,我们需要一种机制把"使用说明"注入到 AI 的上下文里。这就可以使用 Skills

1、demo

下面对之前写好的一个mcp server进行改造,改成CLI模式

知识体系------MCP(四)demo(1)自定义mcp server(http模式&stdio模式)的demo-CSDN博客

security-ai-mcp-demo/

├── pom.xml

├── README.md

└── src/

└── main/

├── java/com/demo/mcp/

│ ├── SecurityAiMcpApplication.java

│ ├── auth/

│ │ └── AppKeyService.java

│ ├── backend/

│ │ └── BackendApiClient.java

│ └── cli/

│ └── CliRunner.java

└── resources/

├── application.properties

└── logback-spring.xml

(1)pom

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.demo</groupId>
    <artifactId>security-ai-mcp-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>security-ai-mcp-demo</name>
    <description>CLI for security-ai-demo /v1 APIs (user/school/role CRUD) with app_key auth</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

(2)配置文件和启动类

复制代码
# CLI 模式:不启动 Web,日志走 stderr
spring.main.web-application-type=none
spring.main.banner-mode=off

backend.base-url=http://localhost:6666/securityAIDemo
backend.user-account=admin
backend.password=123

mcp.app-key=demo-key-001

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- CLI:stdout 留给命令输出,日志输出到 stderr -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.err</target>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

package com.demo.mcp;

import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecurityAiMcpApplication {

    public static void main(String[] args) {
        if (args.length < 1 || !"cli".equals(args[0])) {
            System.err.println("Usage: java -jar security-ai-mcp-demo-<version>.jar cli <resource> <action> [options]");
            System.err.println("  resource: user | school | role");
            System.err.println("  action: list | get | save | update | delete");
            System.err.println("  Set SECURITY_AI_APP_KEY or use --app-key=KEY for auth.");
            System.exit(1);
        }
        SpringApplication app = new SpringApplication(SecurityAiMcpApplication.class);
        app.setBannerMode(Banner.Mode.OFF);
        app.setAdditionalProfiles("cli");
        app.run(args);
    }
}

(3)cli解析

复制代码
package com.demo.mcp.cli;

import com.demo.mcp.auth.AppKeyService;
import com.demo.mcp.backend.BackendApiClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * CLI 模式入口:当以 <code>cli &lt;resource&gt; &lt;action&gt; [options]</code> 启动时运行,
 * 调用 Backend API 并将结果输出到 stdout,供 Cursor Agent 通过 skill+shell 使用。
 * <p>
 * 用法示例:
 * <pre>
 *   java -jar app.jar cli user list --pageNum=1 --pageSize=5 [--account=xxx]
 *   java -jar app.jar cli user get --id=1
 *   java -jar app.jar cli user save --body="{\"account\":\"a\",\"name\":\"n\"}"
 *   java -jar app.jar cli user update --body="{\"id\":1,...}"
 *   java -jar app.jar cli user delete --id=1
 *   java -jar app.jar cli school list|get|save|update|delete  (同上,get/delete 用 --id,save/update 用 --body)
 *   java -jar app.jar cli role list|get|save|update|delete
 * </pre>
 * AppKey(优先级从高到低):--app-key 参数、环境变量 SECURITY_AI_APP_KEY、本地文件 ~/.security-ai-cli(或 %USERPROFILE%\.security-ai-cli)。
 */
@Component
@Profile("cli")
public class CliRunner implements ApplicationRunner {

    private static final Logger log = LoggerFactory.getLogger(CliRunner.class);

    /** 本地 appKey 文件:用户主目录下的 .security-ai-cli,内容为一行 key 或 SECURITY_AI_APP_KEY=xxx */
    private static final String APP_KEY_FILE = ".security-ai-cli";

    private final AppKeyService appKeyService;
    private final BackendApiClient backend;

    public CliRunner(AppKeyService appKeyService, BackendApiClient backend) {
        this.appKeyService = appKeyService;
        this.backend = backend;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        String[] raw = args.getSourceArgs();
        if (raw.length < 3 || !"cli".equals(raw[0])) {
            printUsage();
            exit(1);
            return;
        }
        String resource = raw[1].toLowerCase();
        String action = raw[2].toLowerCase();
        Map<String, String> opts = parseOptions(raw, 3);

        String appKey = opts.get("app-key");
        if (appKey == null || appKey.isBlank()) {
            appKey = System.getenv("SECURITY_AI_APP_KEY");
        }
        if (appKey == null || appKey.isBlank()) {
            appKey = readAppKeyFromFile();
        }
        if (!appKeyService.validate(appKey)) {
            err("Invalid or missing appKey. Use one of: --app-key=KEY, env SECURITY_AI_APP_KEY, or local file ~/.security-ai-cli (must match mcp.app-key in config).");
            exit(1);
            return;
        }

        try {
            String result = dispatch(resource, action, opts);
            if (result != null) {
                System.out.println(result);
            }
            exit(0);
        } catch (IllegalArgumentException e) {
            err(e.getMessage());
            printUsage();
            exit(1);
        } catch (Exception e) {
            log.error("CLI error", e);
            err("Error: " + e.getMessage());
            exit(1);
        }
    }

    private Map<String, String> parseOptions(String[] raw, int start) {
        Map<String, String> m = new HashMap<>();
        for (int i = start; i < raw.length; i++) {
            String s = raw[i];
            if (s.startsWith("--")) {
                String keyVal = s.substring(2);
                int eq = keyVal.indexOf('=');
                if (eq > 0) {
                    m.put(keyVal.substring(0, eq).trim(), keyVal.substring(eq + 1).trim());
                } else if (i + 1 < raw.length && !raw[i + 1].startsWith("--")) {
                    m.put(keyVal.trim(), raw[++i].trim());
                } else {
                    m.put(keyVal.trim(), "");
                }
            }
        }
        return m;
    }

    /** 从用户主目录的 .security-ai-cli 读取 appKey;支持单行 key 或 SECURITY_AI_APP_KEY=xxx */
    private String readAppKeyFromFile() {
        String home = System.getProperty("user.home");
        if (home == null || home.isBlank()) return null;
        Path path = Paths.get(home, APP_KEY_FILE);
        if (!Files.isRegularFile(path)) return null;
        try {
            List<String> lines = Files.readAllLines(path);
            for (String line : lines) {
                String s = line.trim();
                if (s.isEmpty() || s.startsWith("#")) continue;
                if (s.contains("=")) {
                    int eq = s.indexOf('=');
                    String key = s.substring(0, eq).trim();
                    if ("SECURITY_AI_APP_KEY".equals(key) || "appKey".equals(key) || "app-key".equals(key)) {
                        String val = s.substring(eq + 1).trim();
                        if (!val.isEmpty()) return val;
                    }
                } else {
                    return s;
                }
            }
        } catch (Exception e) {
            log.debug("Failed to read appKey from {}: {}", path, e.getMessage());
        }
        return null;
    }

    private String dispatch(String resource, String action, Map<String, String> opts) {
        switch (resource) {
            case "user" -> {
                return dispatchUser(action, opts);
            }
            case "school" -> {
                return dispatchSchool(action, opts);
            }
            case "role" -> {
                return dispatchRole(action, opts);
            }
            default -> throw new IllegalArgumentException("Unknown resource: " + resource + ". Use: user, school, role.");
        }
    }

    private String dispatchUser(String action, Map<String, String> opts) {
        return switch (action) {
            case "list" -> {
                String pageNum = opts.getOrDefault("pagenum", opts.getOrDefault("pageNum", "1"));
                String pageSize = opts.getOrDefault("pagesize", opts.getOrDefault("pageSize", "5"));
                String account = opts.get("account");
                Map<String, String> params = new HashMap<>(Map.of("pageNum", pageNum, "pageSize", pageSize));
                if (account != null && !account.isBlank()) params.put("account", account);
                yield backend.get("/user/list", params);
            }
            case "get" -> requireId(opts, id -> backend.get("/user/" + id, null));
            case "save" -> requireBody(opts, body -> backend.post("/user", body));
            case "update" -> requireBody(opts, body -> backend.put("/user", body));
            case "delete" -> requireId(opts, id -> backend.delete("/user/" + id));
            default -> throw new IllegalArgumentException("Unknown action: " + action + ". Use: list, get, save, update, delete.");
        };
    }

    private String dispatchSchool(String action, Map<String, String> opts) {
        return switch (action) {
            case "list" -> {
                String pageNum = opts.getOrDefault("pagenum", opts.getOrDefault("pageNum", "1"));
                String pageSize = opts.getOrDefault("pagesize", opts.getOrDefault("pageSize", "5"));
                yield backend.get("/school/list", Map.of("pageNum", pageNum, "pageSize", pageSize));
            }
            case "get" -> requireId(opts, id -> backend.get("/school/" + id, null));
            case "save" -> requireBody(opts, body -> backend.post("/school", body));
            case "update" -> requireBody(opts, body -> backend.put("/school", body));
            case "delete" -> requireId(opts, id -> backend.delete("/school/" + id));
            default -> throw new IllegalArgumentException("Unknown action: " + action + ". Use: list, get, save, update, delete.");
        };
    }

    private String dispatchRole(String action, Map<String, String> opts) {
        return switch (action) {
            case "list" -> {
                String pageNum = opts.getOrDefault("pagenum", opts.getOrDefault("pageNum", "1"));
                String pageSize = opts.getOrDefault("pagesize", opts.getOrDefault("pageSize", "5"));
                yield backend.get("/role/list", Map.of("pageNum", pageNum, "pageSize", pageSize));
            }
            case "get" -> requireId(opts, id -> backend.get("/role/" + id, null));
            case "save" -> requireBody(opts, body -> backend.post("/role", body));
            case "update" -> requireBody(opts, body -> backend.put("/role", body));
            case "delete" -> requireId(opts, id -> backend.delete("/role/" + id));
            default -> throw new IllegalArgumentException("Unknown action: " + action + ". Use: list, get, save, update, delete.");
        };
    }

    private String requireId(Map<String, String> opts, java.util.function.Function<String, String> fn) {
        String id = opts.get("id");
        if (id == null || id.isBlank()) throw new IllegalArgumentException("Missing required option: --id=...");
        return fn.apply(id.trim());
    }

    private String requireBody(Map<String, String> opts, java.util.function.Function<String, String> fn) {
        String body = opts.get("body");
        if (body == null || body.isBlank()) throw new IllegalArgumentException("Missing required option: --body='...' (JSON string)");
        return fn.apply(body.trim());
    }

    private void printUsage() {
        String u = """
            Usage: java -jar security-ai-mcp-demo-<version>.jar cli <resource> <action> [options]
            Resources: user, school, role
            Actions: list, get, save, update, delete

            Options (global): --app-key=KEY  or env SECURITY_AI_APP_KEY  or file ~/.security-ai-cli
            List:  --pageNum=1 --pageSize=5  [user list only: --account=xxx]
            Get/Delete: --id=1
            Save/Update: --body='{"key":"value"}'  (JSON string)

            Examples:
              cli user list --pageNum=1 --pageSize=10
              cli user get --id=1
              cli user save --body="{\\"account\\":\\"u1\\",\\"name\\":\\"User 1\\"}"
              cli school list
              cli role get --id=1
            """;
        err(u);
    }

    private void err(String msg) {
        System.err.println(msg);
    }

    private void exit(int code) {
        System.exit(code);
    }
}

(4)接口调用

复制代码
package com.demo.mcp.backend;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;

import java.util.Map;

/**
 * 调用 security-ai-demo 的 /v1 接口。使用配置的服务账号登录获取 JWT,并在请求头中携带 token。
 * 收到 401 时清除 token 并重试一次(应对 token 过期);403/4xx 时抛出带 body 的异常便于排查。
 */
@Component
public class BackendApiClient {

    @Value("${backend.base-url}")
    private String baseUrl;
    @Value("${backend.user-account}")
    private String userAccount;
    @Value("${backend.password}")
    private String password;

    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;

    private volatile String cachedToken;

    public BackendApiClient(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
        this.restTemplate = new RestTemplate();
    }

    private void clearToken() {
        cachedToken = null;
    }

    private String getToken() {
        if (cachedToken != null) {
            return cachedToken;
        }
        synchronized (this) {
            if (cachedToken != null) return cachedToken;
            MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
            form.add("userAccount", userAccount);
            form.add("passWord", password);
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            HttpEntity<MultiValueMap<String, String>> req = new HttpEntity<>(form, headers);
            String loginUrl = baseUrl + "/login";
            ResponseEntity<String> res = restTemplate.exchange(loginUrl, HttpMethod.POST, req, String.class);
            if (res.getStatusCode().is2xxSuccessful() && res.getBody() != null) {
                try {
                    JsonNode root = objectMapper.readTree(res.getBody());
                    JsonNode data = root.path("data");
                    if (!data.isMissingNode() && data.isTextual()) {
                        cachedToken = data.asText();
                        return cachedToken;
                    }
                } catch (Exception e) {
                    throw new RuntimeException("Failed to parse login response: " + e.getMessage());
                }
            }
            throw new RuntimeException("Backend login failed: " + res.getStatusCode());
        }
    }

    private HttpHeaders authHeaders() {
        HttpHeaders h = new HttpHeaders();
        h.set("token", getToken());
        h.setContentType(MediaType.APPLICATION_JSON);
        return h;
    }

    private static String messageFrom(HttpStatusCodeException e) {
        String body = e.getResponseBodyAsString();
        if (body != null && !body.isBlank()) return body;
        return "[no body]";
    }

    private static RuntimeException toRuntime(HttpStatusCodeException e) {
        int code = e.getStatusCode().value();
        String msg = "Backend returned " + code + ": " + messageFrom(e);
        if (code == 403) msg = "Backend 403 Forbidden (token 可能过期或权限不足). " + messageFrom(e);
        if (code == 401) msg = "Backend 401 Unauthorized (登录失效). " + messageFrom(e);
        return new RuntimeException(msg, e);
    }

    public String get(String path, Map<String, String> queryParams) {
        return getImpl(path, queryParams, 0);
    }

    private String getImpl(String path, Map<String, String> queryParams, int retryCount) {
        String url = baseUrl + path;
        if (queryParams != null && !queryParams.isEmpty()) {
            StringBuilder sb = new StringBuilder(url);
            sb.append('?');
            queryParams.forEach((k, v) -> {
                if (v != null) sb.append(k).append('=').append(v).append('&');
            });
            url = sb.substring(0, sb.length() - 1);
        }
        try {
            ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(authHeaders()), String.class);
            return res.getBody();
        } catch (HttpStatusCodeException e) {
            int code = e.getStatusCode().value();
            if ((code == 401 || code == 403) && retryCount == 0) {
                clearToken();
                return getImpl(path, queryParams, 1);
            }
            throw toRuntime(e);
        }
    }

    public String post(String path, String jsonBody) {
        return postImpl(path, jsonBody, 0);
    }

    private String postImpl(String path, String jsonBody, int retryCount) {
        String url = baseUrl + path;
        HttpEntity<String> entity = new HttpEntity<>(jsonBody != null ? jsonBody : "{}", authHeaders());
        try {
            ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
            return res.getBody();
        } catch (HttpStatusCodeException e) {
            int code = e.getStatusCode().value();
            if ((code == 401 || code == 403) && retryCount == 0) {
                clearToken();
                return postImpl(path, jsonBody, 1);
            }
            throw toRuntime(e);
        }
    }

    public String put(String path, String jsonBody) {
        return putImpl(path, jsonBody, 0);
    }

    private String putImpl(String path, String jsonBody, int retryCount) {
        String url = baseUrl + path;
        HttpEntity<String> entity = new HttpEntity<>(jsonBody != null ? jsonBody : "{}", authHeaders());
        try {
            ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.PUT, entity, String.class);
            return res.getBody();
        } catch (HttpStatusCodeException e) {
            int code = e.getStatusCode().value();
            if ((code == 401 || code == 403) && retryCount == 0) {
                clearToken();
                return putImpl(path, jsonBody, 1);
            }
            throw toRuntime(e);
        }
    }

    public String delete(String path) {
        return deleteImpl(path, 0);
    }

    private String deleteImpl(String path, int retryCount) {
        String url = baseUrl + path;
        try {
            ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.DELETE, new HttpEntity<>(authHeaders()), String.class);
            return res.getBody();
        } catch (HttpStatusCodeException e) {
            int code = e.getStatusCode().value();
            if ((code == 401 || code == 403) && retryCount == 0) {
                clearToken();
                return deleteImpl(path, 1);
            }
            throw toRuntime(e);
        }
    }
}

package com.demo.mcp.auth;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 校验入参 appKey 与配置文件中的 mcp.app-key 是否一致。
 */
@Component
public class AppKeyService {

    @Value("${mcp.app-key:}")
    private String appKey;

    public boolean validate(String key) {
        return appKey != null && !appKey.isBlank()
                && appKey.equals(key != null ? key.trim() : "");
    }
}

(5) 测试前先配置好本地鉴权的key:

内容为:

复制代码
demo-key-001

2、CLI 命令测试

复制代码
java -jar c:\mydemo\security-ai-mcp-demo\target\security-ai-mcp-demo-1.0-SNAPSHOT.jar cli user list --pageNum=1 --pageSize=5

c:\mydemo\security-ai-mcp-demo\scripts\security-ai-cli.bat school get --id=1

如果key错误:

3、cursor调用

(1)先配置skill

如我New了两个skill,分别对应上述的user tool和school tool:

① school的skill
复制代码
---
name: security-ai-cli-school
description: >
  School CRUD for security-ai-demo via CLI.
  Use when the user asks about schools, school list, get/add/update/delete school in security-ai-demo.
  Triggers on: 学校列表、查学校、分页查学校、根据 ID 查学校、新增学校、更新学校、删除学校、security-ai 学校、demo 学校.
version: 1.0.0
---

# Security AI Demo --- School CLI

Jar: `SECURITY_AI_CLI_JAR` (env, absolute path) or `c:\mydemo\security-ai-mcp-demo\target\security-ai-mcp-demo-1.0-SNAPSHOT.jar` when workspace is the demo project.  
Auth: `SECURITY_AI_APP_KEY` or `--app-key=...`.

```bash
java -jar "%SECURITY_AI_CLI_JAR%" cli school <action> [options]
```

PowerShell: `$env:SECURITY_AI_CLI_JAR`. Bash: `"$SECURITY_AI_CLI_JAR"`.

---

## Quick Reference

| action | Required | Optional |
|--------|----------|----------|
| list   | ---        | --pageNum=1 --pageSize=5 |
| get    | --id=    | --- |
| delete | --id=    | --- |
| save   | --body='{"name":"..."}' | --- |
| update | --body='{"id":1,"name":"..."}' | --- |

---

## Examples

```bash
java -jar "%SECURITY_AI_CLI_JAR%" cli school list --pageNum=1 --pageSize=5
java -jar "%SECURITY_AI_CLI_JAR%" cli school get --id=1
java -jar "%SECURITY_AI_CLI_JAR%" cli school delete --id=1
java -jar "%SECURITY_AI_CLI_JAR%" cli school save --body="{\"name\":\"School A\"}"
java -jar "%SECURITY_AI_CLI_JAR%" cli school update --body="{\"id\":1,\"name\":\"School A Updated\"}"
```
②user的skill
复制代码
---
name: security-ai-cli-user
description: >
  User CRUD for security-ai-demo via CLI.
  Use when the user asks about users, user list, get/add/update/delete user in security-ai-demo.
  Triggers on: 用户列表、查用户、按 ID/account 查用户、新增用户、更新用户、删除用户、security-ai 用户、demo 用户.
version: 1.0.0
---

# Security AI Demo --- User CLI

Jar: `SECURITY_AI_CLI_JAR` (env, absolute path) or `c:\mydemo\security-ai-mcp-demo\target\security-ai-mcp-demo-1.0-SNAPSHOT.jar` when workspace is the demo project.  
Auth: `SECURITY_AI_APP_KEY` or `--app-key=...`.

```bash
java -jar "%SECURITY_AI_CLI_JAR%" cli user <action> [options]
```

PowerShell: `$env:SECURITY_AI_CLI_JAR`. Bash: `"$SECURITY_AI_CLI_JAR"`.

---

## Quick Reference

| action | Required | Optional |
|--------|----------|----------|
| list   | ---        | --pageNum=1 --pageSize=5 --account=xxx |
| get    | --id=    | --- |
| delete | --id=    | --- |
| save   | --body='{"account":"","name":""...}' | --- |
| update | --body='{"id":1,...}' | --- |

---

## Examples

```bash
java -jar "%SECURITY_AI_CLI_JAR%" cli user list --pageNum=1 --pageSize=10
java -jar "%SECURITY_AI_CLI_JAR%" cli user list --account=admin
java -jar "%SECURITY_AI_CLI_JAR%" cli user get --id=1
java -jar "%SECURITY_AI_CLI_JAR%" cli user delete --id=1
java -jar "%SECURITY_AI_CLI_JAR%" cli user save --body="{\"account\":\"u1\",\"name\":\"User One\"}"
java -jar "%SECURITY_AI_CLI_JAR%" cli user update --body="{\"id\":1,\"account\":\"u1\",\"name\":\"Updated\"}"
```

(2)对话测试

可以看到,调用了对应的skill,执行了cli

相关推荐
阿_旭2 小时前
基于YOLO26深度学习的茶叶病害智能检测识别系统【python源码+Pyqt5界面+数据集+训练代码】
人工智能·python·深度学习·茶叶病害检测
穿过锁扣的风2 小时前
OpenCV 高斯金字塔与拉普拉斯金字塔详解
人工智能·opencv·计算机视觉
天天进步20152 小时前
WrenAI 深度解析:算法视角:wren-ai-service 如何利用 RAG 与 Metadata 提升 SQL 准确率?
人工智能·sql·算法
电商API&Tina2 小时前
1688跨境寻源通API数据采集: 获得1688商品详情关键字搜索商品按图搜索1688商品
大数据·前端·数据库·人工智能·爬虫·json·图搜索算法
荷蒲2 小时前
【小白量化机器人】爬取财经新闻并利用本地大模型评分选择合适交易策略
人工智能·python·机器学习·ai·金融·本地大模型
一起来学吧2 小时前
【OpenClaw系列教程】第七篇:OpenClaw 实战示例 -掌握 AI Agent 的强大能力
人工智能·ai·openclaw
小王努力学编程2 小时前
LangGraph——AI应用开发框架
服务器·人工智能·python·ai·langchain·rag·langgraph
heimeiyingwang2 小时前
OpenClaw 发展趋势:开源 AI 助手的未来之路
人工智能·开源·ai助手
Lalolander2 小时前
生产计划频繁被打乱?利用MES构建动态排程与订单影响分析体系
大数据·人工智能·mes·制造执行系统·工单管理软件·工厂管理软件·生产计划管理