docs(ai): 添加AI集成相关文档和代码

- 添加GitNexus代码智能文档AGENTS.md和CLAUDE.md
- 新增AI模型选择类AiModelSelection用于运行时切换提供商和模型
- 实现AI服务管理器AiServiceManager支持动态选择AI提供商和模型
- 添加ClaudeService实现Anthropic API调用功能
- 创建SkillPromptLoader根据关键词动态加载技能提示词
- 添加基础提示词模板base-prompt.txt指导magic-script编写
- 实现进阶特性提示词skill-advanced.txt包含异步、Lambda等功能
This commit is contained in:
冰点
2026-04-15 11:58:17 +08:00
parent 8fdcec00fb
commit 27b6b93fa8
17 changed files with 1761 additions and 0 deletions

101
AGENTS.md Normal file
View File

@@ -0,0 +1,101 @@
<!-- gitnexus:start -->
# 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: "<error or symptom>"})` — find execution flows related to the issue
2. `gitnexus_context({name: "<suspect function>"})` — 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` |
<!-- gitnexus:end -->

101
CLAUDE.md Normal file
View File

@@ -0,0 +1,101 @@
<!-- gitnexus:start -->
# 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: "<error or symptom>"})` — find execution flows related to the issue
2. `gitnexus_context({name: "<suspect function>"})` — 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` |
<!-- gitnexus:end -->

View File

@@ -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;
}
}

View File

@@ -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<String, MagicAiProperties.ProviderConfig> providerConfigs;
private final ConcurrentHashMap<String, AiService> 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<Map<String, Object>> getConfiguredProviders() {
List<Map<String, Object>> result = new ArrayList<>();
for (Map.Entry<String, MagicAiProperties.ProviderConfig> entry : providerConfigs.entrySet()) {
Map<String, Object> 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);
}
}
}

View File

@@ -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<AiChatMessage> 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<String> entity = new HttpEntity<>(jsonBody, headers);
ResponseEntity<String> 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<String> 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());
}
}
}

View File

@@ -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<String, String> PROMPT_CACHE = new LinkedHashMap<>();
/** Skill 关键词映射:文件名 → 关键词列表 */
private static final Map<String, List<String>> 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", "<if", "<where", "<set", "<foreach",
"条件查询", "模糊", "搜索", "条件", "动态SQL", "动态sql"));
SKILL_KEYWORDS.put("skill-transaction-cache", Arrays.asList(
"事务", "transaction", "commit", "rollback", "缓存", "cache",
"多数据源", "slave", "从库", "数据源"));
SKILL_KEYWORDS.put("skill-request-response", Arrays.asList(
"请求", "参数", "header", "body", "path", "cookie", "getFile",
"上传", "文件", "response", "download", "下载", "exit", "验证",
"校验", "接口", "CRUD", "crud"));
SKILL_KEYWORDS.put("skill-http-client", Arrays.asList(
"http", "调用", "请求外部", "connect", "第三方", "API", "api",
"远程", "外部接口", "对接", "http请求", "HTTP请求"));
SKILL_KEYWORDS.put("skill-collection-ops", Arrays.asList(
"集合", "数组", "map", "filter", "group", "join", "sort", "distinct",
"reduce", "列表操作", "转换", "聚合", "sum", "avg", "tree", "",
"分组", "排序", "去重", "遍历", "toMap"));
SKILL_KEYWORDS.put("skill-linq", Arrays.asList(
"linq", "LINQ", "select.*from", "group by", "order by", "left join",
"关联查询", "内存查询", "集合查询"));
SKILL_KEYWORDS.put("skill-java-interop", Arrays.asList(
"java", "Java", "import", "Date", "Spring", "Bean", "bean",
"类型转换", "::int", "::date", "asString", "asInt", "new ",
"调用Java", "调用java", "自定义函数"));
SKILL_KEYWORDS.put("skill-advanced", Arrays.asList(
"异步", "async", "lambda", "箭头函数", "可选链", "?.",
"展开", "...", "log", "日志", "env", "环境变量",
"try", "catch", "redis", "Redis", "range"));
// 加载所有提示词文件
loadAllPrompts();
}
private static void loadAllPrompts() {
// 加载 base prompt
loadPromptFile("base-prompt");
// 加载所有 skill 文件
for (String skillName : SKILL_KEYWORDS.keySet()) {
loadPromptFile(skillName);
}
}
private static void loadPromptFile(String name) {
String path = PROMPT_DIR + name + ".txt";
try (InputStream is = SkillPromptLoader.class.getClassLoader().getResourceAsStream(path)) {
if (is == null) {
logger.warn("提示词文件未找到: {}", path);
return;
}
String content = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
PROMPT_CACHE.put(name, content);
logger.debug("已加载提示词文件: {}", name);
} catch (Exception e) {
logger.error("加载提示词文件失败: {}", path, e);
}
}
/**
* 根据用户消息构建完整的系统提示词
*
* @param userMessage 用户消息
* @return 组装后的系统提示词base + 匹配的 skills
*/
public static String buildPrompt(String userMessage) {
StringBuilder prompt = new StringBuilder();
// 始终加载 base prompt
String basePrompt = PROMPT_CACHE.get("base-prompt");
if (basePrompt != null) {
prompt.append(basePrompt);
}
// 匹配 skills
List<String> 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<String> 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<String, Integer> hitCounts = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> 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.<String, Integer>comparingByValue().reversed())
.limit(MAX_SKILLS)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
}

View File

@@ -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
- Lambdae => 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/finallycatch(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
✅ 直接用变量名 nameGET参数自动注入作用域
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}
✅ 用 Javaimport '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.iddelete 关键字不能用于删除 map 字段)
✅ 用筛选重建新mapvar updateData = body.filter((k, v) => k != 'id')
## 输出规则
- 代码用 ```magicscript 包裹
- 脚本顶层直接执行,不需要 main 函数
- 最终结果用 return 返回
- 注释简洁,使用中文注释
- 直接 return 数据,框架自动包装为 {code:1, message:'success', data: 你的返回值}
## 标准代码模式参考
查询列表+分页+条件搜索:
```magicscript
return db.page("""
select * from user
<where>
<if test="name != null and name != ''">and name like concat('%',#{name},'%')</if>
<if test="status != null">and status = #{status}</if>
</where>
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
```

View File

@@ -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)
```

View File

@@ -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')
```

View File

@@ -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: '...'}]
```

View File

@@ -0,0 +1,104 @@
## Fluent Table APIdb.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()
```

View File

@@ -0,0 +1,101 @@
## 动态 SQLMyBatis 风格)
在三引号 """...""" SQL 中使用 XML 标签实现动态条件。
### if 标签
```magicscript
return db.select("""
select * from user
<where>
<if test="name != null and name != ''">
and name like concat('%', #{name}, '%')
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="minAge != null">
and age >= #{minAge}
</if>
</where>
order by create_time desc
""")
```
### if / elseif / else
```magicscript
return db.select("""
select * from user where
<if test="type == 'vip'">
level >= 3
</if>
<elseif test="type == 'normal'">
level >= 1
</elseif>
<else>
1 = 1
</else>
""")
```
### where 标签
自动处理第一个 AND/OR避免 WHERE AND 语法错误:
```magicscript
return db.select("""
select * from user
<where>
<if test="name != null">and name = #{name}</if>
<if test="age != null">and age = #{age}</if>
</where>
""")
```
### set 标签(更新)
自动处理末尾多余逗号:
```magicscript
return db.update("""
update user
<set>
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
<if test="status != null">status = #{status},</if>
</set>
where id = #{id}
""")
```
### foreach 标签(批量)
```magicscript
var idList = [1, 2, 3, 4, 5]
return db.select("""
select * from user
where id in
<foreach collection="idList" item="item" open="(" separator="," close=")">
#{item}
</foreach>
""")
```
### 综合示例:条件搜索 + 分页
```magicscript
return db.page("""
select u.*, d.dept_name
from user u
left join department d on d.id = u.dept_id
<where>
<if test="keyword != null and keyword != ''">
and (u.name like concat('%',#{keyword},'%')
or u.phone like concat('%',#{keyword},'%'))
</if>
<if test="deptId != null">
and u.dept_id = #{deptId}
</if>
<if test="startDate != null">
and u.create_time >= #{startDate}
</if>
<if test="endDate != null">
and u.create_time &lt;= #{endDate}
</if>
</where>
order by u.create_time desc
""")
```

View File

@@ -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
```

View File

@@ -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()
```

View File

@@ -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
```

View File

@@ -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字段构建新mapmap不能用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()
```

View File

@@ -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',有效期 60000ms60秒
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')
// 不同数据源支持同样的 APIselect/table 等)
return db.slave.table('user').where().eq('status', 1).select()
```