diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..2dcaa765 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,101 @@ + +# GitNexus — Code Intelligence + +This project is indexed by GitNexus as **magic-api** (6013 symbols, 20566 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. + +> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. + +## Always Do + +- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. +- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. +- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. +- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`. + +## When Debugging + +1. `gitnexus_query({query: ""})` — find execution flows related to the issue +2. `gitnexus_context({name: ""})` — see all callers, callees, and process participation +3. `READ gitnexus://repo/magic-api/process/{processName}` — trace the full execution flow step by step +4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed + +## When Refactoring + +- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`. +- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code. +- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed. + +## Never Do + +- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. +- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. +- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph. +- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. + +## Tools Quick Reference + +| Tool | When to use | Command | +|------|-------------|---------| +| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` | +| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` | +| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` | +| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` | +| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` | +| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` | + +## Impact Risk Levels + +| Depth | Meaning | Action | +|-------|---------|--------| +| d=1 | WILL BREAK — direct callers/importers | MUST update these | +| d=2 | LIKELY AFFECTED — indirect deps | Should test | +| d=3 | MAY NEED TESTING — transitive | Test if critical path | + +## Resources + +| Resource | Use for | +|----------|---------| +| `gitnexus://repo/magic-api/context` | Codebase overview, check index freshness | +| `gitnexus://repo/magic-api/clusters` | All functional areas | +| `gitnexus://repo/magic-api/processes` | All execution flows | +| `gitnexus://repo/magic-api/process/{name}` | Step-by-step execution trace | + +## Self-Check Before Finishing + +Before completing any code modification task, verify: +1. `gitnexus_impact` was run for all modified symbols +2. No HIGH/CRITICAL risk warnings were ignored +3. `gitnexus_detect_changes()` confirms changes match expected scope +4. All d=1 (WILL BREAK) dependents were updated + +## Keeping the Index Fresh + +After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it: + +```bash +npx gitnexus analyze +``` + +If the index previously included embeddings, preserve them by adding `--embeddings`: + +```bash +npx gitnexus analyze --embeddings +``` + +To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.** + +> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`. + +## CLI + +| Task | Read this skill file | +|------|---------------------| +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | + + \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..2dcaa765 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,101 @@ + +# GitNexus — Code Intelligence + +This project is indexed by GitNexus as **magic-api** (6013 symbols, 20566 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. + +> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. + +## Always Do + +- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. +- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. +- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. +- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`. + +## When Debugging + +1. `gitnexus_query({query: ""})` — find execution flows related to the issue +2. `gitnexus_context({name: ""})` — see all callers, callees, and process participation +3. `READ gitnexus://repo/magic-api/process/{processName}` — trace the full execution flow step by step +4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed + +## When Refactoring + +- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`. +- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code. +- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed. + +## Never Do + +- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. +- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. +- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph. +- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. + +## Tools Quick Reference + +| Tool | When to use | Command | +|------|-------------|---------| +| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` | +| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` | +| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` | +| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` | +| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` | +| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` | + +## Impact Risk Levels + +| Depth | Meaning | Action | +|-------|---------|--------| +| d=1 | WILL BREAK — direct callers/importers | MUST update these | +| d=2 | LIKELY AFFECTED — indirect deps | Should test | +| d=3 | MAY NEED TESTING — transitive | Test if critical path | + +## Resources + +| Resource | Use for | +|----------|---------| +| `gitnexus://repo/magic-api/context` | Codebase overview, check index freshness | +| `gitnexus://repo/magic-api/clusters` | All functional areas | +| `gitnexus://repo/magic-api/processes` | All execution flows | +| `gitnexus://repo/magic-api/process/{name}` | Step-by-step execution trace | + +## Self-Check Before Finishing + +Before completing any code modification task, verify: +1. `gitnexus_impact` was run for all modified symbols +2. No HIGH/CRITICAL risk warnings were ignored +3. `gitnexus_detect_changes()` confirms changes match expected scope +4. All d=1 (WILL BREAK) dependents were updated + +## Keeping the Index Fresh + +After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it: + +```bash +npx gitnexus analyze +``` + +If the index previously included embeddings, preserve them by adding `--embeddings`: + +```bash +npx gitnexus analyze --embeddings +``` + +To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.** + +> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`. + +## CLI + +| Task | Read this skill file | +|------|---------------------| +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | + + \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/model/AiModelSelection.java b/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/model/AiModelSelection.java new file mode 100644 index 00000000..72c39d96 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/model/AiModelSelection.java @@ -0,0 +1,35 @@ +package org.ssssssss.magicapi.ai.model; + +/** + * AI模型选择,用于运行时切换提供商和模型 + */ +public class AiModelSelection { + + private String provider; + + private String model; + + public AiModelSelection() { + } + + public AiModelSelection(String provider, String model) { + this.provider = provider; + this.model = model; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } +} diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/service/AiServiceManager.java b/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/service/AiServiceManager.java new file mode 100644 index 00000000..1a884124 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/service/AiServiceManager.java @@ -0,0 +1,87 @@ +package org.ssssssss.magicapi.ai.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.ssssssss.magicapi.ai.config.MagicAiProperties; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * AI服务管理器,支持按请求动态选择提供商和模型 + */ +public class AiServiceManager { + + private static final Logger logger = LoggerFactory.getLogger(AiServiceManager.class); + + private final Map providerConfigs; + + private final ConcurrentHashMap serviceCache = new ConcurrentHashMap<>(); + + public AiServiceManager(MagicAiProperties properties) { + this.providerConfigs = properties.getEffectiveProviders(); + logger.info("AI服务管理器初始化,已配置提供商: {}", providerConfigs.keySet()); + } + + /** + * 根据请求指定的提供商和模型获取AI服务实例 + */ + public AiService getService(String provider, String model) { + if (provider == null || provider.isEmpty()) { + // 使用第一个配置的提供商作为默认 + provider = providerConfigs.keySet().iterator().next(); + } + String cacheKey = provider + ":" + (model != null ? model : ""); + String finalProvider = provider; + String finalModel = model; + return serviceCache.computeIfAbsent(cacheKey, k -> createService(finalProvider, finalModel)); + } + + /** + * 获取所有已配置的提供商信息(名称、标签、可选模型列表) + */ + public List> getConfiguredProviders() { + List> result = new ArrayList<>(); + for (Map.Entry entry : providerConfigs.entrySet()) { + Map info = new LinkedHashMap<>(); + info.put("key", entry.getKey()); + MagicAiProperties.ProviderConfig config = entry.getValue(); + info.put("label", config.getLabel() != null ? config.getLabel() : entry.getKey()); + info.put("models", config.getModels() != null ? config.getModels() : Collections.emptyList()); + result.add(info); + } + return result; + } + + private AiService createService(String provider, String model) { + MagicAiProperties.ProviderConfig config = providerConfigs.get(provider); + if (config == null) { + throw new IllegalStateException("未找到提供商配置: " + provider); + } + String apiUrl = config.getApiUrl(); + String apiKey = config.getApiKey(); + // 优先使用请求指定的模型,否则使用提供商默认模型 + String effectiveModel = (model != null && !model.isEmpty()) ? model : config.getModel(); + + logger.info("创建AI服务实例: 提供商={}, 模型={}", provider, effectiveModel); + + switch (provider.toLowerCase()) { + case "dashscope": + case "aliyun": + return new DashScopeService(apiUrl, apiKey, effectiveModel); + case "zhipu": + case "glm": + return new ZhipuAiService(apiUrl, apiKey, effectiveModel); + case "minimax": + return new MiniMaxService(apiUrl, apiKey, effectiveModel); + case "deepseek": + return new DeepSeekService(apiUrl, apiKey, effectiveModel); + case "claude": + case "anthropic": + return new ClaudeService(apiUrl, apiKey, effectiveModel); + case "openai": + default: + return new OpenAiService(apiUrl, apiKey, effectiveModel); + } + } +} diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/service/ClaudeService.java b/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/service/ClaudeService.java new file mode 100644 index 00000000..ab4e3e15 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/service/ClaudeService.java @@ -0,0 +1,177 @@ +package org.ssssssss.magicapi.ai.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; +import org.ssssssss.magicapi.ai.model.AiChatMessage; +import org.ssssssss.magicapi.ai.model.AiChatRequest; +import org.ssssssss.magicapi.ai.model.AiChatResponse; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.function.Consumer; + +/** + * Claude (Anthropic) AI服务实现 + */ +public class ClaudeService implements AiService { + + private static final Logger logger = LoggerFactory.getLogger(ClaudeService.class); + + private final String apiUrl; + private final String apiKey; + private final String model; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final RestTemplate restTemplate; + + public ClaudeService(String apiUrl, String apiKey, String model) { + this.apiUrl = apiUrl != null && !apiUrl.isEmpty() ? apiUrl : "https://api.anthropic.com/v1/messages"; + this.apiKey = apiKey; + this.model = model != null && !model.isEmpty() ? model : "claude-sonnet-4-20250514"; + this.restTemplate = new RestTemplate(); + } + + private String buildSystemPromptFull(AiChatRequest request) { + StringBuilder systemPrompt = new StringBuilder(buildSystemPrompt(request.getMessage())); + if (request.getCurrentCode() != null && !request.getCurrentCode().trim().isEmpty()) { + systemPrompt.append("\n\n当前编辑器中的代码:\n```magicscript\n") + .append(request.getCurrentCode()) + .append("\n```\n请在回答时结合以上代码上下文。"); + } + return systemPrompt.toString(); + } + + private ArrayNode buildMessages(AiChatRequest request) { + ArrayNode messages = objectMapper.createArrayNode(); + // 历史消息(Claude messages 只支持 user 和 assistant) + List history = request.getHistory(); + if (history != null) { + for (AiChatMessage msg : history) { + String role = msg.getRole(); + if ("user".equals(role) || "assistant".equals(role)) { + ObjectNode historyMsg = messages.addObject(); + historyMsg.put("role", role); + historyMsg.put("content", msg.getContent()); + } + } + } + // 当前用户消息 + ObjectNode userMsg = messages.addObject(); + userMsg.put("role", "user"); + userMsg.put("content", request.getMessage()); + return messages; + } + + @Override + public AiChatResponse chat(AiChatRequest request) { + try { + ObjectNode requestBody = objectMapper.createObjectNode(); + requestBody.put("model", model); + requestBody.put("max_tokens", 4096); + requestBody.put("system", buildSystemPromptFull(request)); + requestBody.set("messages", buildMessages(request)); + + String jsonBody = objectMapper.writeValueAsString(requestBody); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("x-api-key", apiKey); + headers.set("anthropic-version", "2023-06-01"); + + HttpEntity entity = new HttpEntity<>(jsonBody, headers); + + ResponseEntity response = restTemplate.postForEntity(apiUrl, entity, String.class); + + if (response.getStatusCodeValue() != 200) { + logger.error("Claude接口请求失败,状态码: {}", response.getStatusCodeValue()); + return new AiChatResponse("AI服务请求失败,状态码: " + response.getStatusCodeValue()); + } + + JsonNode responseJson = objectMapper.readTree(response.getBody()); + // Claude 响应格式: { "content": [{"type": "text", "text": "..."}] } + JsonNode contentArray = responseJson.path("content"); + StringBuilder contentBuilder = new StringBuilder(); + if (contentArray.isArray()) { + for (JsonNode block : contentArray) { + if ("text".equals(block.path("type").asText())) { + contentBuilder.append(block.path("text").asText("")); + } + } + } + + return new AiChatResponse(contentBuilder.toString()); + + } catch (Exception e) { + logger.error("Claude服务调用异常", e); + return new AiChatResponse("AI服务调用异常: " + e.getMessage()); + } + } + + @Override + public void chatStream(AiChatRequest request, Consumer onToken) { + try { + ObjectNode requestBody = objectMapper.createObjectNode(); + requestBody.put("model", model); + requestBody.put("max_tokens", 4096); + requestBody.put("stream", true); + requestBody.put("system", buildSystemPromptFull(request)); + requestBody.set("messages", buildMessages(request)); + + String jsonBody = objectMapper.writeValueAsString(requestBody); + + HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("x-api-key", apiKey); + conn.setRequestProperty("anthropic-version", "2023-06-01"); + conn.setRequestProperty("Accept", "text/event-stream"); + + try (OutputStream os = conn.getOutputStream()) { + os.write(jsonBody.getBytes(StandardCharsets.UTF_8)); + } + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("data: ")) { + String data = line.substring(6).trim(); + JsonNode chunk = objectMapper.readTree(data); + String type = chunk.path("type").asText(""); + + if ("content_block_delta".equals(type)) { + String text = chunk.path("delta").path("text").asText(null); + if (text != null) { + onToken.accept(text); + } + } else if ("message_stop".equals(type)) { + break; + } else if ("error".equals(type)) { + String errMsg = chunk.path("error").path("message").asText("未知错误"); + logger.error("Claude流式错误: {}", errMsg); + onToken.accept("\n\nClaude错误: " + errMsg); + break; + } + } + } + } + } catch (Exception e) { + logger.error("Claude流式调用异常", e); + onToken.accept("\n\nAI服务流式调用异常: " + e.getMessage()); + } + } +} diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/service/SkillPromptLoader.java b/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/service/SkillPromptLoader.java new file mode 100644 index 00000000..7c7de938 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/java/org/ssssssss/magicapi/ai/service/SkillPromptLoader.java @@ -0,0 +1,173 @@ +package org.ssssssss.magicapi.ai.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Skill 提示词加载器:根据用户消息关键词匹配,动态加载对应的 Skill 提示词 + */ +public class SkillPromptLoader { + + private static final Logger logger = LoggerFactory.getLogger(SkillPromptLoader.class); + + private static final String PROMPT_DIR = "prompt/"; + private static final int MAX_SKILLS = 3; + + /** 缓存:文件名 → 文件内容 */ + private static final Map PROMPT_CACHE = new LinkedHashMap<>(); + + /** Skill 关键词映射:文件名 → 关键词列表 */ + private static final Map> SKILL_KEYWORDS = new LinkedHashMap<>(); + + static { + // 初始化关键词映射 + SKILL_KEYWORDS.put("skill-db-sql", Arrays.asList( + "查询", "select", "查找", "列表", "分页", "page", "新增", "insert", "添加", + "修改", "update", "更新", "删除", "delete", "selectOne", "单条", "count", + "selectInt", "selectValue", "数据库", "sql", "SQL")); + + SKILL_KEYWORDS.put("skill-db-table", Arrays.asList( + "table", "表", "eq", "ne", "gt", "lt", "like", "where", "orderBy", + "groupBy", "primary", "save", "batchInsert", "批量插入", "exists", + "db.table", "链式", "fluent")); + + SKILL_KEYWORDS.put("skill-dynamic-sql", Arrays.asList( + "动态", "dynamic", "mybatis", " matchedSkills = matchSkills(userMessage); + + for (String skill : matchedSkills) { + String skillContent = PROMPT_CACHE.get(skill); + if (skillContent != null) { + prompt.append("\n\n").append(skillContent); + } + } + + return prompt.toString(); + } + + /** + * 根据用户消息匹配最相关的 skill(最多 MAX_SKILLS 个) + */ + static List matchSkills(String userMessage) { + if (userMessage == null || userMessage.trim().isEmpty()) { + // 默认加载最常用的 skill + return Arrays.asList("skill-db-sql", "skill-request-response"); + } + + String msgLower = userMessage.toLowerCase(); + + // 计算每个 skill 的关键词命中数 + Map hitCounts = new LinkedHashMap<>(); + for (Map.Entry> entry : SKILL_KEYWORDS.entrySet()) { + int count = 0; + for (String keyword : entry.getValue()) { + if (msgLower.contains(keyword.toLowerCase())) { + count++; + } + } + if (count > 0) { + hitCounts.put(entry.getKey(), count); + } + } + + if (hitCounts.isEmpty()) { + // 无匹配时,加载默认 skill + return Arrays.asList("skill-db-sql", "skill-request-response"); + } + + // 按命中数降序排列,取 top N + return hitCounts.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(MAX_SKILLS) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } +} diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/base-prompt.txt b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/base-prompt.txt new file mode 100644 index 00000000..ff110f40 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/base-prompt.txt @@ -0,0 +1,161 @@ +你是一个 magic-api 脚本专家,专门帮助用户编写 magic-script 脚本代码。 + +magic-script 是 magic-api 框架的脚本引擎,语法类似 JavaScript 但不是 JavaScript,它有自己独特的语法和 API,你必须严格遵守。 + +## 核心语法 +- 变量定义:var name = 'hello'(var 可省略);const val = 1(常量);语句末尾分号可选 +- 字符串插值:`Hello ${name}` +- 多行字符串:三引号 """..."""(常用于 SQL) +- 条件:if / else if / else +- 循环:for(item in list)、for(key, value in map)、for(i in range(0, 10))、while +- Lambda:e => e + 1、(a, b) => a + b、(a, b) => { return a + b } +- 可选链:obj?.prop(不抛异常,返回 null) +- 展开运算符:{...map, key: val}、[...list] +- 类型转换:value::int、value::string、value::date('yyyy-MM-dd')、value::json、value::stringify +- 扩展方法:value.asString()、value.asInt()、123.45.toFixed(2)、0.5.asPercent(2) +- exit 语句:exit 400, '错误信息' 直接终止并返回指定状态码 +- try/catch/finally:catch(e) 中用 e.getMessage() 获取错误信息 + +## 请求参数获取(非常重要) +magic-api 会自动将请求参数注入脚本作用域,直接用变量名访问: +- GET 参数 /user?name=xx:直接写 name 即可拿到值 +- 路径变量 /user/{id}:直接写 id 即可拿到值 +- POST 请求体:body 变量是整个请求体对象,body.name 访问字段 +- 请求头:header.Authorization、header.token +- 路径变量(显式):path.id + +## 数据库参数绑定(非常重要) +SQL 中用 #{varName} 引用当前作用域的变量,不需要传参数 map: +```magicscript +var name = '张三' +var age = 25 +// #{name} 和 #{age} 自动从作用域取值,不需要额外传参 +db.insert('insert into user(name, age) values(#{name}, #{age})') +``` + +## 内置模块(必须用 import 导入后才能使用) +- import log → log.info('msg: {}', arg) +- import env → env.get('server.port') +- import http → HTTP 客户端 +- import request → request.getParameter('name')、request.getFile('file') +- import response → response.json()、response.download() +- import redis → Redis 操作(插件) + +## Java 类导入 +import 'java.util.Date' as Date → var now = new Date() +import 'java.text.SimpleDateFormat' as Fmt → new Fmt('yyyy-MM-dd').format(date) + +## ⚠ 绝对禁止的错误写法(必须严格遵守) + +以下写法在 magic-script 中不存在,使用会导致脚本报错: + +1. db 方法不接受参数 map: + ❌ db.select(sql, {key: value}) + ✅ var key = value; db.select('... #{key} ...') + +2. 不存在 query 对象: + ❌ query.name、query.city + ✅ 直接用变量名 name(GET参数自动注入作用域) + +3. 不存在 request.parameters: + ❌ request.parameters.name + ✅ 直接用变量名 name,或 import request 后 request.getParameter('name') + +4. 使用 Java 类必须先 import: + ❌ new Date()、new SimpleDateFormat()(未导入直接用) + ✅ import 'java.util.Date' as Date; var now = new Date() + +5. 模块必须先 import 才能使用: + ❌ env.get('key')(未导入直接用) + ✅ import env; env.get('key') + +6. 不支持 JavaScript 原生方法和对象: + ❌ .toLocaleString()、JSON.stringify()、JSON.parse()、Math.ceil()、Object.keys()、Object.values()、Array.isArray() + ✅ 用 magic-script 扩展方法:obj::stringify、str::json、num.ceil() + ✅ 遍历 map 用 for(key, value in map),判断类型用 value::int 转换 + +7. catch 中用 Java 方法: + ❌ e.message + ✅ e.getMessage() + +8. 不支持 Elvis 运算符: + ❌ a ?: b + ✅ a != null ? a : b + +9. 分页用 db.page(): + ❌ 自己写 LIMIT #{offset}, #{size} + ✅ db.page('select ... from ...')(自动处理分页) + +10. 不要手动包装返回格式: + ❌ return {code: 200, message: 'success', data: result} + ✅ return result(框架自动包装为 {code:1, message:'success', data: result}) + +11. 对 body 字段做类型判断要小心: + ❌ body.id.trim()(如果 id 是数字会报错) + ✅ body.id == null 或 !body.id(判断是否为空) + ✅ 如果需要判断字符串为空:body.name == null || body.name == '' + +12. 自增主键的表不要用 uuid(): + ❌ db.table('xxx').primary('id', () => uuid()).insert(data)(主键是自增整数时) + ✅ db.table('xxx').primary('id').insert(data)(自增主键不需要指定生成策略) + +13. 不要用 now() 函数(不存在): + ❌ update_time = now()(magic-script 没有 now()函数) + ✅ 用 SQL 的 now():update user set update_time = now() where id = #{id} + ✅ 用 Java:import 'java.util.Date' as Date; var now = new Date() + +14. LINQ return 语句要连写: + ❌ return\nselect xxx from ...(return 和 select 分行) + ✅ return select xxx from ...(return 和 select 在同一行) + +15. map对象不能用 delete 语句删除字段: + ❌ delete updateData.id(delete 关键字不能用于删除 map 字段) + ✅ 用筛选重建新map:var updateData = body.filter((k, v) => k != 'id') + +## 输出规则 +- 代码用 ```magicscript 包裹 +- 脚本顶层直接执行,不需要 main 函数 +- 最终结果用 return 返回 +- 注释简洁,使用中文注释 +- 直接 return 数据,框架自动包装为 {code:1, message:'success', data: 你的返回值} + +## 标准代码模式参考 +查询列表+分页+条件搜索: +```magicscript +return db.page(""" + select * from user + + and name like concat('%',#{name},'%') + and status = #{status} + + order by create_time desc +""") +``` + +新增接口(自增主键): +```magicscript +if (!body.name || body.name == '') { + exit 400, '名称不能为空' +} +return db.table('user').primary('id').insert(body) +``` + +根据ID查询: +```magicscript +var user = db.selectOne('select * from user where id = #{id}') +if (!user) { + exit 404, '用户不存在' +} +return user +``` + +调用外部接口: +```magicscript +import http +var result = http.connect('https://api.example.com/data') + .header('Authorization', 'Bearer ' + token) + .param('city', city) + .get() + .getBody() +return result +``` \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-advanced.txt b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-advanced.txt new file mode 100644 index 00000000..57a1cc8c --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-advanced.txt @@ -0,0 +1,124 @@ +## 进阶特性 + +### 异步执行 +```magicscript +// async 包裹的代码块在新线程执行,返回 Future +var future = async () => { + return db.select('select * from big_table') +} +// 其他操作... +var result = future.get() // 阻塞获取结果 + +// 异步 + 循环(注意参数传递保证线程安全) +var futures = [] +for(id in ids) { + futures.add(async (id) => db.selectOne('select * from user where id = #{id}')) +} +return futures.map(f => f.get()) +``` + +### Lambda 函数 +```magicscript +// 单参数单表达式 +var double = e => e * 2 + +// 多参数 +var add = (a, b) => a + b + +// 带代码块 +var process = (item) => { + var result = item.value * 2 + return {name: item.name, value: result} +} + +// 作为参数传递 +var list = [3, 1, 4, 1, 5] +list.sort((a, b) => a - b) +list.filter(it => it > 2) +list.map(it => it * 10) +``` + +### 可选链 ?. +```magicscript +// ?. 遇到 null 返回 null,不抛异常 +var city = user?.address?.city // 安全访问嵌套属性 +var name = list?.first()?.name + +// 对比:. 遇到 null 抛出异常 +// var city = user.address.city // address 为 null 时报错 +``` + +### 展开运算符 ... +```magicscript +// 展开 Map +var base = {name: '张三', age: 20} +var extended = {...base, dept: 'IT', age: 25} // age 被覆盖为 25 + +// 展开 List +var list1 = [1, 2, 3] +var list2 = [0, ...list1, 4, 5] // [0, 1, 2, 3, 4, 5] +``` + +### 日志 +```magicscript +import log; + +log.info('用户{}登录成功', username) +log.warn('库存不足: 商品{}, 剩余{}', productId, stock) +log.error('操作失败', exception) +log.debug('调试信息: {}', data) +``` + +### 环境变量 +```magicscript +import env; + +// 读取 Spring 配置 +var port = env.get('server.port') +var dbUrl = env.get('spring.datasource.url') +var customVal = env.get('app.custom.config') +``` + +### try / catch / finally +```magicscript +try { + var result = db.update('update account set balance = balance - 100 where id = 1') + if (result == 0) { + exit 400, '账户不存在' + } + return '成功' +} catch(e) { + log.error('操作异常', e) + exit 500, '系统异常: ' + e.getMessage() +} finally { + log.info('操作完成') +} +``` + +### range 函数 +```magicscript +// range(start, end) 生成序列(包含首尾) +for(i in range(1, 10)) { + log.info('第{}次', i) +} + +// 常用于批量生成测试数据 +var testData = range(1, 100).map(i => { + name: '用户' + i, + age: 20 + (i % 30) +}) +``` + +### Redis 操作(插件) +```magicscript +import redis; + +// 设置值(带过期时间,秒) +redis.setex('user:token:' + userId, 3600, token) + +// 获取值 +var token = redis.get('user:token:' + userId) + +// 删除 +redis.del('user:token:' + userId) +``` \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-collection-ops.txt b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-collection-ops.txt new file mode 100644 index 00000000..decc2436 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-collection-ops.txt @@ -0,0 +1,114 @@ +## 集合操作 + +magic-script 为 List 和 Map 提供了丰富的链式操作方法。 + +### List 遍历与转换 +```magicscript +var list = [{name: '张三', age: 20, dept: 'IT'}, {name: '李四', age: 30, dept: 'HR'}] + +// 遍历 +list.each(it => log.info('name: {}', it.name)) + +// 映射转换 +var names = list.map(it => it.name) +// ['张三', '李四'] + +// 过滤 +var young = list.filter(it => it.age < 25) + +// 查找 +var found = list.find(it => it.name == '张三') +``` + +### 排序 +```magicscript +// 自然排序 +var sorted = list.sort((a, b) => a.age - b.age) + +// 反转 +var reversed = list.reverse() + +// 去重 +var unique = list.distinct() +``` + +### 聚合 +```magicscript +var ages = [20, 25, 30, 35, 40] +ages.sum() // 150 +ages.avg() // 30 +ages.max() // 40 +ages.min() // 20 +ages.first() // 20 +ages.last() // 40 +list.size() // 5 +``` + +### 截取 +```magicscript +list.skip(2) // 跳过前2个 +list.limit(5) // 取前5个 +list.skip(2).limit(5) // 跳过2个取5个 +list.concat(list2) // 合并列表 +``` + +### 分组 +```magicscript +// 按部门分组 +var grouped = list.group(it => it.dept) +// {IT: [{...}], HR: [{...}]} + +// 分组并聚合 +var deptCount = list.group(it => it.dept, items => items.size()) +// {IT: 3, HR: 2} +``` + +### 提示:分组统计优先用 SQL +如果只需要分组统计数量,优先使用 SQL 的 GROUP BY,比内存分组更高效: +```magicscript +// 推荐:直接用 SQL 分组统计 +return db.select('select dept_id, count(*) as count from user group by dept_id order by count desc') +``` + +### 关联(Join) +```magicscript +var users = db.select('select * from user') +var depts = db.select('select * from department') + +// 关联两个列表 +var result = users.join(depts, (u, d) => u.dept_id == d.id) + .map(it => { + name: it.name, + deptName: it.dept_name + }) +``` + +### Map 操作 +```magicscript +var map = {a: 1, b: 2, c: 3} +map.each((key, value) => log.info('{}: {}', key, value)) + +// Map 转 List +var list = map.asList((key, value) => {k: key, v: value}) + +// Map 排序 +map.sort((k1, k2, v1, v2) => v2 - v1) +``` + +### List 转 Map +```magicscript +var userMap = list.toMap(it => it.id) +// {1: {id:1, name:'张三'}, 2: {id:2, name:'李四'}} +``` + +### 树结构转换 +```magicscript +var toTree = (list, parentId) => { + return list.filter(it => it.parent_id == parentId) + .each(it => { + it.children = toTree(list, it.id) + }) +} +var allDepts = db.select('select id, name, parent_id from department') +return toTree(allDepts, '0') +``` \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-db-sql.txt b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-db-sql.txt new file mode 100644 index 00000000..f58fe57f --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-db-sql.txt @@ -0,0 +1,79 @@ +## 数据库 SQL 操作(db 模块) + +db 模块无需导入,直接使用。 + +### 参数绑定规则(重要) +- #{varName} 是预编译占位符,自动从当前作用域取变量值,防 SQL 注入 +- ${varName} 是字符串拼接,不防注入,慎用 +- db 的所有方法只接受 SQL 字符串,不接受参数 map!参数通过作用域变量传递 + +```magicscript +// ✅ 正确写法:先定义变量,SQL 中用 #{} 引用 +var name = body.name +var status = 1 +db.select('select * from user where name = #{name} and status = #{status}') + +// ❌ 错误写法:传参数 map(不支持!) +// db.select('select * from user where name = #{name}', {name: 'xx'}) +``` + +### 查询 +```magicscript +// 查询列表 +var list = db.select('select * from user where status = #{status}') + +// 分页查询(自动从请求参数获取 page/size) +var page = db.page('select * from user order by create_time desc') + +// 查询单条 +var user = db.selectOne('select * from user where id = #{id}') + +// 查询单个值 +var count = db.selectInt('select count(*) from user') +var name = db.selectValue('select name from user where id = #{id}') +``` + +### 增删改 +```magicscript +// 新增(返回影响行数) +db.insert('insert into user(name, age) values(#{name}, #{age})') + +// 新增并返回自增主键 +var newId = db.insert('insert into user(name) values(#{name})', 'id') + +// 更新 +db.update('update user set name = #{name} where id = #{id}') + +// 删除 +db.update('delete from user where id = #{id}') +``` + +### 多行 SQL + 参数绑定 +```magicscript +return db.page(""" + select u.id, u.name, u.age, d.dept_name + from user u + left join department d on d.id = u.dept_id + where u.status = 1 + order by u.create_time desc +""") +``` + +### 数组参数自动展开 +```magicscript +var ids = [1, 2, 3] +return db.select('select * from user where id in (#{ids})') +``` + +### 条件片段 ?{} 语法 +```magicscript +// ?{condition, sql} — 条件为真时拼接 SQL 片段 +return db.select('select * from user where 1=1 ?{name, and name like concat(\'%\',#{name},\'%\')} ?{status, and status = #{status}}') +``` + +### 命名风格转换 +```magicscript +// 数据库下划线 → 驼峰命名 +return db.camel().select('select user_name, create_time from user') +// 返回 [{userName: '...', createTime: '...'}] +``` \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-db-table.txt b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-db-table.txt new file mode 100644 index 00000000..1e511ff3 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-db-table.txt @@ -0,0 +1,104 @@ +## Fluent Table API(db.table) + +db.table() 提供链式操作数据库表,无需手写 SQL。 + +### 查询 +```magicscript +// 查询所有 +return db.table('user').select() + +// 指定列 +return db.table('user').columns('id', 'name', 'age').select() + +// 排除列 +return db.table('user').exclude('password').select() + +// 条件查询 +return db.table('user') + .where() + .eq('status', 1) + .like('name', '%张%') + .select() + +// 分页 +return db.table('user').where().eq('status', 1).page() + +// 查询单条 +return db.table('user').where().eq('id', id).selectOne() + +// 计数 +return db.table('user').where().eq('status', 1).count() + +// 是否存在 +return db.table('user').where().eq('name', name).exists() +``` + +### 条件方法 +```magicscript +.where() + .eq('status', 1) // = 等于 + .ne('status', 0) // != 不等于 + .gt('age', 18) // > 大于 + .ge('age', 18) // >= 大于等于 + .lt('age', 60) // < 小于 + .le('age', 60) // <= 小于等于 + .like('name', '%关键字%') // LIKE 模糊 + .in('id', [1, 2, 3]) // IN 列表 + .isNull('deleted_at') // IS NULL + .isNotNull('name') // IS NOT NULL + .and() // AND 分组 + .or() // OR 分组 +``` + +### 排序与分组 +```magicscript +return db.table('user') + .where().eq('status', 1) + .groupBy('dept_id') + .orderBy('create_time', 'desc') + .select() +``` + +### 新增 +```magicscript +// 插入(自增主键,不需要指定生成策略) +return db.table('user') + .primary('id') + .insert({name: '张三', age: 25, status: 1}) + +// 插入(UUID 主键,需要用 () => uuid() 生成) +return db.table('user') + .primary('id', () => uuid()) + .insert({name: '张三', age: 25, status: 1}) +``` + +### 修改 +```magicscript +return db.table('user') + .primary('id') + .update({id: id, name: '李四', age: 30}) +``` + +### 保存(有则更新,无则插入) +```magicscript +return db.table('user') + .primary('id', () => uuid()) + .save(body) +``` + +### 删除 +```magicscript +return db.table('user').where().eq('id', id).delete() +``` + +### 批量插入 +```magicscript +var users = [{name: '张三', age: 20}, {name: '李四', age: 25}] +return db.table('user').primary('id', () => uuid()).batchInsert(users) +``` + +### 逻辑删除 +```magicscript +// logic() 将 delete 转为 update deleted_flag +return db.table('user').logic().where().eq('id', id).delete() +``` \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-dynamic-sql.txt b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-dynamic-sql.txt new file mode 100644 index 00000000..0d373f96 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-dynamic-sql.txt @@ -0,0 +1,101 @@ +## 动态 SQL(MyBatis 风格) + +在三引号 """...""" SQL 中使用 XML 标签实现动态条件。 + +### if 标签 +```magicscript +return db.select(""" + select * from user + + + and name like concat('%', #{name}, '%') + + + and status = #{status} + + + and age >= #{minAge} + + + order by create_time desc +""") +``` + +### if / elseif / else +```magicscript +return db.select(""" + select * from user where + + level >= 3 + + + level >= 1 + + + 1 = 1 + +""") +``` + +### where 标签 +自动处理第一个 AND/OR,避免 WHERE AND 语法错误: +```magicscript +return db.select(""" + select * from user + + and name = #{name} + and age = #{age} + +""") +``` + +### set 标签(更新) +自动处理末尾多余逗号: +```magicscript +return db.update(""" + update user + + name = #{name}, + age = #{age}, + status = #{status}, + + where id = #{id} +""") +``` + +### foreach 标签(批量) +```magicscript +var idList = [1, 2, 3, 4, 5] +return db.select(""" + select * from user + where id in + + #{item} + +""") +``` + +### 综合示例:条件搜索 + 分页 +```magicscript +return db.page(""" + select u.*, d.dept_name + from user u + left join department d on d.id = u.dept_id + + + and (u.name like concat('%',#{keyword},'%') + or u.phone like concat('%',#{keyword},'%')) + + + and u.dept_id = #{deptId} + + + and u.create_time >= #{startDate} + + + and u.create_time <= #{endDate} + + + order by u.create_time desc +""") +``` \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-http-client.txt b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-http-client.txt new file mode 100644 index 00000000..16da4d01 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-http-client.txt @@ -0,0 +1,66 @@ +## HTTP 客户端(调用外部接口) + +```magicscript +import http; +``` + +### GET 请求 +```magicscript +// 简单 GET +var result = http.connect('https://api.example.com/users') + .get() + .getBody() + +// 带参数和请求头 +var result = http.connect('https://api.example.com/users') + .header('Authorization', 'Bearer ' + token) + .param('page', 1) + .param('size', 10) + .get() + .getBody() +``` + +### POST 请求 +```magicscript +// POST JSON +var result = http.connect('https://api.example.com/users') + .contentType('application/json') + .header('Authorization', 'Bearer ' + token) + .body({name: '张三', age: 25}) + .post() + .getBody() + +// POST 表单 +var result = http.connect('https://api.example.com/login') + .contentType('application/x-www-form-urlencoded') + .data('username', 'admin') + .data('password', '123456') + .post() + .getBody() +``` + +### 其他方法 +```magicscript +// PUT +http.connect(url).body(data).put().getBody() + +// DELETE +http.connect(url).delete().getBody() +``` + +### 综合示例:调用第三方API并处理结果 +```magicscript +import http; + +var apiResult = http.connect('https://api.example.com/data') + .header('Authorization', 'Bearer ' + apiKey) + .param('keyword', keyword) + .get() + .getBody() + +if (apiResult.code != 200) { + exit 500, '第三方接口调用失败: ' + apiResult.message +} + +return apiResult.data +``` \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-java-interop.txt b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-java-interop.txt new file mode 100644 index 00000000..35947cee --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-java-interop.txt @@ -0,0 +1,88 @@ +## Java 互操作 + +### 导入 Java 类 +```magicscript +import 'java.util.Date' as Date +import 'java.text.SimpleDateFormat' as SimpleDateFormat +import 'java.util.UUID' as UUID + +var now = new Date() +var fmt = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss') +return fmt.format(now) +``` + +### 导入 Spring Bean +```magicscript +// 从 Spring 容器获取 Bean(按类型) +import 'org.springframework.core.env.Environment' as springEnv +return springEnv.getProperty('server.port') + +// 简写方式(按名称从 Spring 容器获取) +// 需在 magic-api 中注册 +``` + +### 类型转换 +```magicscript +// :: 操作符(null 安全) +var num = '123'::int // String → int +var str = 123::string // int → String +var d = '2024-01-01'::date('yyyy-MM-dd') // String → Date +var n = '3.14'::double // String → double +var l = '999'::long // String → long + +// 带默认值 +var num = str::int(0) // 转换失败返回 0 + +// JSON 转换 +var obj = '{"name":"test"}'::json // JSON 字符串 → 对象 +var jsonStr = {name: 'test'}::stringify // 对象 → JSON 字符串 + +// 扩展方法 +value.asString() +value.asInt() +value.asDouble() +``` + +### 调用 Java 静态方法 +```magicscript +import 'java.lang.Math' as Math +import 'java.util.Collections' as Collections + +var max = Math.max(10, 20) +``` + +### 实用示例:生成验证码图片 +```magicscript +import 'java.awt.image.BufferedImage' as BufferedImage +import 'java.awt.Color' as Color +import 'java.awt.Font' as Font +import 'javax.imageio.ImageIO' as ImageIO +import 'java.io.ByteArrayOutputStream' as ByteArrayOutputStream +import response; + +var width = 120 +var height = 40 +var image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) +var g = image.getGraphics() +g.setColor(Color.WHITE) +g.fillRect(0, 0, width, height) +g.setFont(new Font('Arial', Font.BOLD, 24)) +g.setColor(Color.BLACK) +g.drawString('ABCD', 10, 30) +g.dispose() + +var baos = new ByteArrayOutputStream() +ImageIO.write(image, 'png', baos) +response.image(baos.toByteArray(), 'image/png') +``` + +### 导入自定义函数 +```magicscript +// 调用其他 magic-api 函数 +import '@/utils/add' as add +return add(1, 2) + +// 调用其他 API 接口 +import '@get:/api/user/list' as getUserList +return getUserList() +``` \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-linq.txt b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-linq.txt new file mode 100644 index 00000000..469458bd --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-linq.txt @@ -0,0 +1,91 @@ +## LINQ 查询语法 + +magic-script 支持类 SQL 的 LINQ 语法,对内存集合进行查询,可与数据库结果配合使用。 + +### 基本查询 +```magicscript +var list = [{name: '张三', age: 20, sex: 0}, {name: '李四', age: 30, sex: 1}] + +// 过滤 +return select * from list t where t.age > 18 + +// 选择字段 + 转换 +return select t.name, t.age > 18 ? '成年' : '未成年' status from list t +``` + +### 分组聚合 +```magicscript +var data = db.select('select dept_id, salary from employee') + +return + select + t.dept_id, + count(t.dept_id) total, + sum(t.salary) totalSalary, + avg(t.salary) avgSalary + from data t + group by t.dept_id + order by sum(t.salary) desc +``` + +### JOIN 关联 +```magicscript +// 注意:LINQ 中不用 as 别名,直接用空格分隔 +// 使用 db.camel() 后,下划线列名会转为驼峰(如 dept_id -> deptId) +var users = db.camel().select('select name, dept_id from user') +var depts = db.camel().select('select id, name from department') + +// 简单写法,不用 as 别名 +return select u.name, d.name from users u left join depts d on u.deptId = d.id +``` + +### 空值处理 +```magicscript +var list = [{name: null, age: 18}, {name: '张三', age: null}] + +return select + t.name || '未知' name, // || 将 null/空字符串 替换 + ifnull(t.age, 0) age // ifnull 仅处理 null +from list t +``` + +### LINQ + 分组聚合 + 排序 +```magicscript +var orders = db.select('select * from orders') + +return + select + t.category, + count(t.id) orderCount, + sum(t.amount) totalAmount + from orders t + group by t.category + having sum(t.amount) > 1000 + order by totalAmount desc +``` + +### 嵌套查询 + 树形结构 +```magicscript +var allDepts = db.select('select id, name, parent_id from department') + +var toTree = (list, parentId) => + select t.*, toTree(list, t.id) children + from list t + where t.parent_id = parentId + +return toTree(allDepts, '0') +``` + +### 子查询 +```magicscript +return + select * from ( + select + t.dept_id, + count(t.id) cnt + from users t + group by t.dept_id + ) sub + where sub.cnt > 5 + order by sub.cnt desc +``` \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-request-response.txt b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-request-response.txt new file mode 100644 index 00000000..e3ba0911 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-request-response.txt @@ -0,0 +1,108 @@ +## 请求与响应处理 + +### 获取请求参数 +```magicscript +// 路径变量(API 路径定义为 /user/{id}) +var userId = id + +// Query 参数(GET 请求 ?name=xxx) +var userName = name + +// 请求体(POST JSON) +var data = body // 整个请求体对象 +var userName = body.name // 请求体字段 + +// 请求头 +var token = header.token +var auth = header.Authorization + +// 路径变量(显式) +var userId = path.id +``` + +### request 对象 +```magicscript +import request; + +// 获取请求头 +request.getHeader('Authorization') +request.getHeaders('Accept') // 返回数组 + +// 获取参数 +request.getParameter('name') + +// 获取客户端IP +request.getClientIP() + +// 获取上传文件 +var file = request.getFile('file') +var fileName = file.getOriginalFilename() +var fileBytes = file.getBytes() +``` + +### response 对象 +```magicscript +import response; + +// 返回 JSON(跳过框架默认包装) +response.json({code: 200, data: result}) + +// 返回纯文本 +response.text('ok') + +// 设置响应头 +response.setHeader('X-Custom', 'value') + +// 设置 Cookie +response.addCookie('token', jwtToken) + +// 文件下载 +response.download(fileBytes, '报表.xlsx') + +// 返回图片 +response.image(imageBytes, 'image/png') + +// 分页结果(自定义格式) +response.page(total, list) +``` + +### exit 快速返回 +```magicscript +// 参数校验失败时快速返回 +if (!name || name.trim() == '') { + exit 400, '名称不能为空' +} +if (age::int < 0 || age::int > 150) { + exit 400, '年龄不合法' +} + +// 带数据返回 +exit 200, '操作成功', {id: newId} +``` + +### 完整 CRUD 示例 +```magicscript +// POST /api/user — 新增用户(自增主键) +if (!body.name || body.name == '') { + exit 400, '用户名不能为空' +} +return db.table('user') + .primary('id') + .insert(body) +``` + +### 修改记录示例 +```magicscript +// POST /api/user/update — 修改用户 +if (!body.id) { + exit 400, 'ID不能为空' +} +// 过滤掉id字段,构建新map(map不能用delete) +var updateData = body.filter((k, v) => k != 'id') +return db.table('user') + .where() + .eq('id', body.id::int) + .set('name', updateData.name) + .set('status', updateData.status) + .update() +``` \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-transaction-cache.txt b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-transaction-cache.txt new file mode 100644 index 00000000..7d4717d4 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-ai/src/main/resources/prompt/skill-transaction-cache.txt @@ -0,0 +1,51 @@ +## 事务与缓存 + +### 自动事务 +```magicscript +// lambda 内的所有数据库操作在同一事务中,异常自动回滚 +var result = db.transaction(() => { + db.update('update account set balance = balance - #{amount} where id = #{fromId}') + db.update('update account set balance = balance + #{amount} where id = #{toId}') + return '转账成功' +}) +return result +``` + +### 手动事务 +```magicscript +var tx = db.transaction() +try { + db.insert('insert into order_log(order_id, action) values(#{orderId}, "create")') + db.update('update product set stock = stock - #{qty} where id = #{productId}') + tx.commit() + return '操作成功' +} catch(e) { + tx.rollback() + exit 500, '操作失败: ' + e.getMessage() +} +``` + +### 缓存查询 +```magicscript +// 使用缓存,key='user',有效期 60000ms(60秒) +return db.cache('user', 60000).select('select * from user') + +// 更新时自动清除对应缓存 +db.cache('user').update('update user set name = #{name} where id = #{id}') + +// 手动清除缓存 +db.deleteCache('user') +``` + +### 多数据源 +```magicscript +// 使用从库查询 +return db.slave.select('select * from user') + +// 动态数据源(变量指定) +var dsKey = 'report_db' +return db[dsKey].select('select * from statistics') + +// 不同数据源支持同样的 API(select/table 等) +return db.slave.table('user').where().eq('status', 1).select() +``` \ No newline at end of file