diff --git a/registry.json b/registry.json index ea952a7..56c0474 100644 --- a/registry.json +++ b/registry.json @@ -1,4 +1,14 @@ { "schema_version": 1, - "plugins": [] + "plugins": [ + { + "id": "request-trace", + "name": "上游请求追踪", + "version": "1.0.0", + "description": "为上游请求加入不含敏感信息的部署追踪标记,便于确认插件链和网关路由。", + "manifest_url": "https://git.9nn.cn/xiaodong/99code-plugins/raw/branch/main/request-trace/manifest.json", + "platforms": ["openai", "codex", "anthropic", "gemini", "antigravity", "grok", "xai", "kimi"], + "sha256": "20639523163a8253b8589dbf65ded5c1efe22c5534093e5ce729c9d181b08630" + } + ] } diff --git a/request-trace/README.md b/request-trace/README.md new file mode 100644 index 0000000..94aad41 --- /dev/null +++ b/request-trace/README.md @@ -0,0 +1,10 @@ +# 请求追踪插件 + +这是 99code 插件商店的首个可安装生产插件。启用后,它会在上游请求中加入不含敏感信息的 `X-99Code-Plugin: request-trace/1.0.0` 标记,便于在反向代理、网关和上游抓包中确认插件链确实生效。 + +可在插件中心配置: + +- `header_name`:必须以 `X-` 开头,最长 64 字符。 +- `header_value`:最长 128 字符,禁止换行。 + +该插件不会读取、记录或上传账号凭据。 diff --git a/request-trace/manifest.json b/request-trace/manifest.json new file mode 100644 index 0000000..a78f8da --- /dev/null +++ b/request-trace/manifest.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "id": "request-trace", + "name": "上游请求追踪", + "version": "1.0.0", + "description": "为上游请求加入不含敏感信息的部署追踪标记,便于确认插件链和网关路由。", + "entrypoint": "com.ninecode.plugins.RequestTracePlugin", + "platforms": [ + "openai", + "codex", + "anthropic", + "gemini", + "antigravity", + "grok", + "xai", + "kimi" + ], + "artifact": { + "url": "https://git.9nn.cn/xiaodong/99code-plugins/raw/branch/main/request-trace/request-trace-1.0.0.jar", + "sha256": "20639523163a8253b8589dbf65ded5c1efe22c5534093e5ce729c9d181b08630", + "signature": "", + "key_id": "", + "size": 2457 + }, + "credential_sync": null, + "metadata": { + "category": "observability", + "homepage": "https://git.9nn.cn/xiaodong/99code-plugins" + } +} diff --git a/request-trace/request-trace-1.0.0.jar b/request-trace/request-trace-1.0.0.jar new file mode 100644 index 0000000..8338d27 Binary files /dev/null and b/request-trace/request-trace-1.0.0.jar differ diff --git a/request-trace/src/main/java/com/ninecode/plugins/RequestTracePlugin.java b/request-trace/src/main/java/com/ninecode/plugins/RequestTracePlugin.java new file mode 100644 index 0000000..828b55c --- /dev/null +++ b/request-trace/src/main/java/com/ninecode/plugins/RequestTracePlugin.java @@ -0,0 +1,63 @@ +package com.ninecode.plugins; + +import com.ninecode.app.plugin.PluginRelayExchange; +import com.ninecode.app.plugin.ProviderPlugin; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** Adds a non-sensitive deployment marker to outbound provider requests. */ +public final class RequestTracePlugin implements ProviderPlugin { + private static final Set SUPPORTED_PLATFORMS = Set.of( + "openai", "codex", "anthropic", "gemini", "antigravity", "grok", "xai", "kimi" + ); + private String headerName = "X-99Code-Plugin"; + private String headerValue = "request-trace/1.0.0"; + + @Override + public String id() { + return "request-trace"; + } + + @Override + public Set platforms() { + return SUPPORTED_PLATFORMS; + } + + @Override + public void start(Map configuration) { + String configuredName = text(configuration, "header_name"); + String configuredValue = text(configuration, "header_value"); + if (!configuredName.isBlank()) { + if (!configuredName.toLowerCase(Locale.ROOT).startsWith("x-") || configuredName.length() > 64) { + throw new IllegalArgumentException("header_name 必须是长度不超过 64 的 X- 自定义请求头"); + } + headerName = configuredName; + } + if (!configuredValue.isBlank()) { + if (configuredValue.length() > 128 || configuredValue.contains("\r") || configuredValue.contains("\n")) { + throw new IllegalArgumentException("header_value 不能为空、换行或超过 128 个字符"); + } + headerValue = configuredValue; + } + } + + @Override + public PluginRelayExchange beforeRequest(PluginRelayExchange exchange) { + Map> headers = new LinkedHashMap<>(exchange.headers()); + headers.putIfAbsent(headerName, List.of(headerValue)); + return new PluginRelayExchange( + exchange.pluginId(), exchange.platform(), exchange.accountId(), exchange.model(), exchange.method(), + exchange.uri(), headers, exchange.body(), exchange.statusCode() + ); + } + + private String text(Map configuration, String key) { + if (configuration == null || configuration.get(key) == null) { + return ""; + } + return String.valueOf(configuration.get(key)).trim(); + } +}