From 3bd16056e4c28e879d37bda4fe00e22162011687 Mon Sep 17 00:00:00 2001 From: zhanghaidong Date: Sun, 19 Jul 2026 19:48:35 +0800 Subject: [PATCH] =?UTF-8?q?7=E6=9C=8819=E6=8F=90=E4=BA=A4=EF=BC=9A?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E9=A6=96=E4=B8=AA=E4=B8=8A=E6=B8=B8=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E8=BF=BD=E8=B8=AA=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- registry.json | 12 +++- request-trace/README.md | 10 +++ request-trace/manifest.json | 30 +++++++++ request-trace/request-trace-1.0.0.jar | Bin 0 -> 2457 bytes .../ninecode/plugins/RequestTracePlugin.java | 63 ++++++++++++++++++ 5 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 request-trace/README.md create mode 100644 request-trace/manifest.json create mode 100644 request-trace/request-trace-1.0.0.jar create mode 100644 request-trace/src/main/java/com/ninecode/plugins/RequestTracePlugin.java 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 0000000000000000000000000000000000000000..8338d278964f21e1aa4fb3daee5050bdeed820be GIT binary patch literal 2457 zcmaKudpMJSAIC>x%V`)txsWDngkg!4B8M`Y)7A))!$e_h6~7Lm*v}yqKZ+uyIc{_k zVR_7XHflNM5OauDIYfz+XZ!VB@$_7t_kG>h^|`P6{r-Nw_xJwe^>MP_Ahrnr00IEo z*>j$NH39*|02mvbrJkLGE#gNj0N^CBLI6M@@PDijLF|_mW9eXLYlFqpc6SfPSQtVbnP(Q)5T>j21Z@Tmq`4}Fibm37w&L449!BoS-Ndq zW8)*^qJo|hK;<}h-T^@e3&Abi!_V!)f7`B@;H@7q1hKwD`ah1)piqJz(Vy^ttct%| z!-7vA4+e04RqE3e%Bj{(uTpvCSx9T)HC zemz)Lu%M+$`B?bfNd`%L+kW)o!kdNAAIk+3CV=-Z$77{~WO{9`SeaRU(XYgOq4?`f zmV^%EiJ>X;Mu)L&B+AZ+H%cO+^|2v$#@RiSn8X4YM6%djJgeoD1tcYIR%&j;*e&vClhTN3wSnKDE>w%A$^F$szp2FAw7SKxtZ2kb642UCdrBGPom|EDiTt z{m<$hI8CY&Zxb{ATC^H=Of)d4Gwk&zVQ1ItB`)yLPqY&mp(qf z%5o=mUpmRdOHR@W&C5KSS98#?yTv&*b3&bEUi>XdIryV`X<$Npo|~=qm0OQ}Y%7Wb zBkBes*!X zw>G&{!bEo7n=xv=Stm04fqFf*c9 zJC8k^qokKA9PX_Pe~ls_8KraO{=R$oYQ0;1x4${2oWu7@b7$t8AUSr8uu5`TejksX zml;^=A=3g?k-NK3%{SqMkv#H3n_{watGN{muF_nVf*LW^hTgJl)7)k_Yix)nU31}P zxhg(lkEFK+3+W4Q<|RiQRpWQrJ`Y0ZO)=DHyT6;uO_OPiB92=c@lh7z+V)$C@?B|DsQDC6gwPi%2Kw?QV+G`1dsax%>MAn(ZZu9Xv?Q8$B)Xr4zm-kgl( zzb&9CquIe(;91bYZU^ea=odgg^W4D4I>VB!fCo!wM7(DqK>v4V)|yt+{u-uQGo+zO$IpgUW z#SwLaIE!S*Yyk}#R)|>diF9dB%k zRTN3+NBOdp?5Wm?UQ^J-1TUa>c4+T~{C6?sg^w*-RGPA)%mc*IKI=4Zj~EL(+|cMO zwQtDK(^@6})8_a)2=4IlddE}TvKx_iGThYWlLIWOR?x&o&~A@wl;Q9?XoY6z5~7TuUff8qvjwTDwID~7!XV+ zaO|Fw^JmmN^b+>B(6hS-7dO7@G|hMA@^epPeN>;AUH6WZoH9+M#)a$dlT0bpdQH=Z z(*7Xy=Y7(r^VtY$;9k%a)Ez~7I4uf0M>P(ozmJ|wIX@rPof@gPmG=l*p*67M0BrEM zMRbyHPJ$=r(oM(R8A&8gL1mwGChRF5@-l|UlpblfT^gD(Gr0W&`QeGt2YgsIWB@bp zI4mhO;3}Wug$qH%->;IqP1`g#4PJ`4-jN)#FcY9s{w0@kF(j83SBkINS#_p^p!BLF zyELMDhN)m(AfLaN232~cQ5~Ok^a{c4(&zc$(fcKFrPo%}hnb4YSLv7P&Y2$5Z0N|7 z-BWTiR5f`tazaCirQ>FKQoF0R)tY{$x2d20HB 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(); + } +}