ParamOverrideEditorModal.jsx 132 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useCallback, useEffect, useMemo, useState } from 'react';
  16. import { useTranslation } from 'react-i18next';
  17. import {
  18. Button,
  19. Card,
  20. Col,
  21. Collapse,
  22. Input,
  23. Modal,
  24. Row,
  25. Select,
  26. Space,
  27. Switch,
  28. Tag,
  29. TextArea,
  30. Typography,
  31. } from '@douyinfe/semi-ui';
  32. import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
  33. import { copy, showError, showSuccess, verifyJSON } from '../../../../helpers';
  34. import {
  35. CLAUDE_CLI_HEADER_PASSTHROUGH_TEMPLATE,
  36. CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE,
  37. } from '../../../../constants/channel-affinity-template.constants';
  38. const { Text } = Typography;
  39. const OPERATION_MODE_OPTIONS = [
  40. { label: '设置字段', value: 'set' },
  41. { label: '删除字段', value: 'delete' },
  42. { label: '追加到末尾', value: 'append' },
  43. { label: '追加到开头', value: 'prepend' },
  44. { label: '复制字段', value: 'copy' },
  45. { label: '移动字段', value: 'move' },
  46. { label: '字符串替换', value: 'replace' },
  47. { label: '正则替换', value: 'regex_replace' },
  48. { label: '裁剪前缀', value: 'trim_prefix' },
  49. { label: '裁剪后缀', value: 'trim_suffix' },
  50. { label: '确保前缀', value: 'ensure_prefix' },
  51. { label: '确保后缀', value: 'ensure_suffix' },
  52. { label: '去掉空白', value: 'trim_space' },
  53. { label: '转小写', value: 'to_lower' },
  54. { label: '转大写', value: 'to_upper' },
  55. { label: '返回自定义错误', value: 'return_error' },
  56. { label: '清理对象项', value: 'prune_objects' },
  57. { label: '请求头透传', value: 'pass_headers' },
  58. { label: '字段同步', value: 'sync_fields' },
  59. { label: '设置请求头', value: 'set_header' },
  60. { label: '删除请求头', value: 'delete_header' },
  61. { label: '复制请求头', value: 'copy_header' },
  62. { label: '移动请求头', value: 'move_header' },
  63. ];
  64. const OPERATION_MODE_VALUES = new Set(
  65. OPERATION_MODE_OPTIONS.map((item) => item.value),
  66. );
  67. const CONDITION_MODE_OPTIONS = [
  68. { label: '完全匹配', value: 'full' },
  69. { label: '前缀匹配', value: 'prefix' },
  70. { label: '后缀匹配', value: 'suffix' },
  71. { label: '包含', value: 'contains' },
  72. { label: '大于', value: 'gt' },
  73. { label: '大于等于', value: 'gte' },
  74. { label: '小于', value: 'lt' },
  75. { label: '小于等于', value: 'lte' },
  76. ];
  77. const CONDITION_MODE_VALUES = new Set(
  78. CONDITION_MODE_OPTIONS.map((item) => item.value),
  79. );
  80. const MODE_META = {
  81. delete: { path: true },
  82. set: { path: true, value: true, keepOrigin: true },
  83. append: { path: true, value: true, keepOrigin: true },
  84. prepend: { path: true, value: true, keepOrigin: true },
  85. copy: { from: true, to: true },
  86. move: { from: true, to: true },
  87. replace: { path: true, from: true, to: false },
  88. regex_replace: { path: true, from: true, to: false },
  89. trim_prefix: { path: true, value: true },
  90. trim_suffix: { path: true, value: true },
  91. ensure_prefix: { path: true, value: true },
  92. ensure_suffix: { path: true, value: true },
  93. trim_space: { path: true },
  94. to_lower: { path: true },
  95. to_upper: { path: true },
  96. return_error: { value: true },
  97. prune_objects: { pathOptional: true, value: true },
  98. pass_headers: { value: true, keepOrigin: true },
  99. sync_fields: { from: true, to: true },
  100. set_header: { path: true, value: true, keepOrigin: true },
  101. delete_header: { path: true },
  102. copy_header: { from: true, to: true, keepOrigin: true, pathAlias: true },
  103. move_header: { from: true, to: true, keepOrigin: true, pathAlias: true },
  104. };
  105. const VALUE_REQUIRED_MODES = new Set([
  106. 'trim_prefix',
  107. 'trim_suffix',
  108. 'ensure_prefix',
  109. 'ensure_suffix',
  110. 'set_header',
  111. 'return_error',
  112. 'prune_objects',
  113. 'pass_headers',
  114. ]);
  115. const FROM_REQUIRED_MODES = new Set([
  116. 'copy',
  117. 'move',
  118. 'replace',
  119. 'regex_replace',
  120. 'copy_header',
  121. 'move_header',
  122. 'sync_fields',
  123. ]);
  124. const TO_REQUIRED_MODES = new Set([
  125. 'copy',
  126. 'move',
  127. 'copy_header',
  128. 'move_header',
  129. 'sync_fields',
  130. ]);
  131. const MODE_DESCRIPTIONS = {
  132. set: '把值写入目标字段',
  133. delete: '删除目标字段',
  134. append: '把值追加到数组 / 字符串 / 对象末尾',
  135. prepend: '把值追加到数组 / 字符串 / 对象开头',
  136. copy: '把来源字段复制到目标字段',
  137. move: '把来源字段移动到目标字段',
  138. replace: '在目标字段里做字符串替换',
  139. regex_replace: '在目标字段里做正则替换',
  140. trim_prefix: '去掉字符串前缀',
  141. trim_suffix: '去掉字符串后缀',
  142. ensure_prefix: '确保字符串有指定前缀',
  143. ensure_suffix: '确保字符串有指定后缀',
  144. trim_space: '去掉字符串头尾空白',
  145. to_lower: '把字符串转成小写',
  146. to_upper: '把字符串转成大写',
  147. return_error: '立即返回自定义错误',
  148. prune_objects: '按条件清理对象中的子项',
  149. pass_headers: '把指定请求头透传到上游请求',
  150. sync_fields: '在一个字段有值、另一个缺失时自动补齐',
  151. set_header: '设置运行期请求头(支持整值覆盖,或用 JSON 映射按逗号 token 替换/删除)',
  152. delete_header: '删除运行期请求头',
  153. copy_header: '复制请求头',
  154. move_header: '移动请求头',
  155. };
  156. const getModePathLabel = (mode) => {
  157. if (mode === 'set_header' || mode === 'delete_header') {
  158. return '请求头名称';
  159. }
  160. if (mode === 'prune_objects') {
  161. return '目标路径(可选)';
  162. }
  163. return '目标字段路径';
  164. };
  165. const getModePathPlaceholder = (mode) => {
  166. if (mode === 'set_header') return 'Authorization';
  167. if (mode === 'delete_header') return 'X-Debug-Mode';
  168. if (mode === 'prune_objects') return 'messages';
  169. return 'temperature';
  170. };
  171. const getModeFromLabel = (mode) => {
  172. if (mode === 'replace') return '匹配文本';
  173. if (mode === 'regex_replace') return '正则表达式';
  174. if (mode === 'copy_header' || mode === 'move_header') return '来源请求头';
  175. return '来源字段';
  176. };
  177. const getModeFromPlaceholder = (mode) => {
  178. if (mode === 'replace') return 'openai/';
  179. if (mode === 'regex_replace') return '^gpt-';
  180. if (mode === 'copy_header' || mode === 'move_header') return 'Authorization';
  181. return 'model';
  182. };
  183. const getModeToLabel = (mode) => {
  184. if (mode === 'replace' || mode === 'regex_replace') return '替换为';
  185. if (mode === 'copy_header' || mode === 'move_header') return '目标请求头';
  186. return '目标字段';
  187. };
  188. const getModeToPlaceholder = (mode) => {
  189. if (mode === 'replace') return '(可留空)';
  190. if (mode === 'regex_replace') return 'openai/gpt-';
  191. if (mode === 'copy_header' || mode === 'move_header') return 'X-Upstream-Auth';
  192. return 'original_model';
  193. };
  194. const getModeValueLabel = (mode) => {
  195. if (mode === 'set_header') return '请求头值(支持字符串或 JSON 映射)';
  196. if (mode === 'pass_headers') return '透传请求头(支持逗号分隔或 JSON 数组)';
  197. if (
  198. mode === 'trim_prefix' ||
  199. mode === 'trim_suffix' ||
  200. mode === 'ensure_prefix' ||
  201. mode === 'ensure_suffix'
  202. ) {
  203. return '前后缀文本';
  204. }
  205. if (mode === 'prune_objects') {
  206. return '清理规则(字符串或 JSON 对象)';
  207. }
  208. return '值(支持 JSON 或普通文本)';
  209. };
  210. const getModeValuePlaceholder = (mode) => {
  211. if (mode === 'set_header') {
  212. return [
  213. 'String example:',
  214. 'Bearer sk-xxx',
  215. '',
  216. 'JSON map example:',
  217. '{"advanced-tool-use-2025-11-20": null, "computer-use-2025-01-24": "computer-use-2025-01-24"}',
  218. '',
  219. 'JSON map wildcard:',
  220. '{"*": null, "computer-use-2025-11-24": "computer-use-2025-11-24"}',
  221. ].join('\n');
  222. }
  223. if (mode === 'pass_headers') return 'Authorization, X-Request-Id';
  224. if (
  225. mode === 'trim_prefix' ||
  226. mode === 'trim_suffix' ||
  227. mode === 'ensure_prefix' ||
  228. mode === 'ensure_suffix'
  229. ) {
  230. return 'openai/';
  231. }
  232. if (mode === 'prune_objects') {
  233. return '{"type":"redacted_thinking"}';
  234. }
  235. return '0.7';
  236. };
  237. const getModeValueHelp = (mode) => {
  238. if (mode !== 'set_header') return '';
  239. return '字符串:整条请求头直接覆盖。JSON 映射:按逗号分隔 token 逐项处理,null 表示删除,string/array 表示替换,* 表示兜底规则。';
  240. };
  241. const SYNC_TARGET_TYPE_OPTIONS = [
  242. { label: '请求体字段', value: 'json' },
  243. { label: '请求头字段', value: 'header' },
  244. ];
  245. const LEGACY_TEMPLATE = {
  246. temperature: 0,
  247. max_tokens: 1000,
  248. };
  249. const OPERATION_TEMPLATE = {
  250. operations: [
  251. {
  252. description: 'Set default temperature for openai/* models.',
  253. path: 'temperature',
  254. mode: 'set',
  255. value: 0.7,
  256. conditions: [
  257. {
  258. path: 'model',
  259. mode: 'prefix',
  260. value: 'openai/',
  261. },
  262. ],
  263. logic: 'AND',
  264. },
  265. ],
  266. };
  267. const HEADER_PASSTHROUGH_TEMPLATE = {
  268. operations: [
  269. {
  270. description: 'Pass through X-Request-Id header to upstream.',
  271. mode: 'pass_headers',
  272. value: ['X-Request-Id'],
  273. keep_origin: true,
  274. },
  275. ],
  276. };
  277. const GEMINI_IMAGE_4K_TEMPLATE = {
  278. operations: [
  279. {
  280. description:
  281. 'Set imageSize to 4K when model contains gemini/image and ends with 4k.',
  282. mode: 'set',
  283. path: 'generationConfig.imageConfig.imageSize',
  284. value: '4K',
  285. conditions: [
  286. {
  287. path: 'original_model',
  288. mode: 'contains',
  289. value: 'gemini',
  290. },
  291. {
  292. path: 'original_model',
  293. mode: 'contains',
  294. value: 'image',
  295. },
  296. {
  297. path: 'original_model',
  298. mode: 'suffix',
  299. value: '4k',
  300. },
  301. ],
  302. logic: 'AND',
  303. },
  304. ],
  305. };
  306. const AWS_BEDROCK_ANTHROPIC_COMPAT_TEMPLATE = {
  307. operations: [
  308. {
  309. description: 'Normalize anthropic-beta header tokens for Bedrock compatibility.',
  310. mode: 'set_header',
  311. path: 'anthropic-beta',
  312. // https://github.com/BerriAI/litellm/blob/main/litellm/anthropic_beta_headers_config.json
  313. value: {
  314. 'advanced-tool-use-2025-11-20': 'tool-search-tool-2025-10-19',
  315. bash_20241022: null,
  316. bash_20250124: null,
  317. 'code-execution-2025-08-25': null,
  318. 'compact-2026-01-12': 'compact-2026-01-12',
  319. 'computer-use-2025-01-24': 'computer-use-2025-01-24',
  320. 'computer-use-2025-11-24': 'computer-use-2025-11-24',
  321. 'context-1m-2025-08-07': 'context-1m-2025-08-07',
  322. 'context-management-2025-06-27': 'context-management-2025-06-27',
  323. 'effort-2025-11-24': null,
  324. 'fast-mode-2026-02-01': null,
  325. 'files-api-2025-04-14': null,
  326. 'fine-grained-tool-streaming-2025-05-14': null,
  327. 'interleaved-thinking-2025-05-14': 'interleaved-thinking-2025-05-14',
  328. 'mcp-client-2025-11-20': null,
  329. 'mcp-client-2025-04-04': null,
  330. 'mcp-servers-2025-12-04': null,
  331. 'output-128k-2025-02-19': null,
  332. 'structured-output-2024-03-01': null,
  333. 'prompt-caching-scope-2026-01-05': null,
  334. 'skills-2025-10-02': null,
  335. 'structured-outputs-2025-11-13': null,
  336. text_editor_20241022: null,
  337. text_editor_20250124: null,
  338. 'token-efficient-tools-2025-02-19': null,
  339. 'tool-search-tool-2025-10-19': 'tool-search-tool-2025-10-19',
  340. 'web-fetch-2025-09-10': null,
  341. 'web-search-2025-03-05': null,
  342. },
  343. },
  344. {
  345. description: 'Remove all tools[*].custom.input_examples before upstream relay.',
  346. mode: 'delete',
  347. path: 'tools.*.custom.input_examples',
  348. },
  349. ],
  350. };
  351. const TEMPLATE_GROUP_OPTIONS = [
  352. { label: '基础模板', value: 'basic' },
  353. { label: '场景模板', value: 'scenario' },
  354. ];
  355. const TEMPLATE_PRESET_CONFIG = {
  356. operations_default: {
  357. group: 'basic',
  358. label: '新格式模板(规则集)',
  359. kind: 'operations',
  360. payload: OPERATION_TEMPLATE,
  361. },
  362. legacy_default: {
  363. group: 'basic',
  364. label: '旧格式模板(JSON 对象)',
  365. kind: 'legacy',
  366. payload: LEGACY_TEMPLATE,
  367. },
  368. pass_headers_auth: {
  369. group: 'scenario',
  370. label: '请求头透传(X-Request-Id)',
  371. kind: 'operations',
  372. payload: HEADER_PASSTHROUGH_TEMPLATE,
  373. },
  374. gemini_image_4k: {
  375. group: 'scenario',
  376. label: 'Gemini 图片 4K',
  377. kind: 'operations',
  378. payload: GEMINI_IMAGE_4K_TEMPLATE,
  379. },
  380. claude_cli_headers_passthrough: {
  381. group: 'scenario',
  382. label: 'Claude CLI 请求头透传',
  383. kind: 'operations',
  384. payload: CLAUDE_CLI_HEADER_PASSTHROUGH_TEMPLATE,
  385. },
  386. codex_cli_headers_passthrough: {
  387. group: 'scenario',
  388. label: 'Codex CLI 请求头透传',
  389. kind: 'operations',
  390. payload: CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE,
  391. },
  392. aws_bedrock_anthropic_beta_override: {
  393. group: 'scenario',
  394. label: 'AWS Bedrock Claude 兼容模板',
  395. kind: 'operations',
  396. payload: AWS_BEDROCK_ANTHROPIC_COMPAT_TEMPLATE,
  397. },
  398. };
  399. const FIELD_GUIDE_TARGET_OPTIONS = [
  400. { label: '填入目标路径', value: 'path' },
  401. { label: '填入来源字段', value: 'from' },
  402. { label: '填入目标字段', value: 'to' },
  403. ];
  404. const BUILTIN_FIELD_SECTIONS = [
  405. {
  406. title: '常用请求字段',
  407. fields: [
  408. {
  409. key: 'model',
  410. label: '模型名称',
  411. tip: '支持多级模型名,例如 openai/gpt-4o-mini',
  412. },
  413. { key: 'temperature', label: '采样温度', tip: '控制输出随机性' },
  414. { key: 'max_tokens', label: '最大输出 Token', tip: '控制输出长度上限' },
  415. { key: 'messages.-1.content', label: '最后一条消息内容', tip: '常用于重写用户输入' },
  416. ],
  417. },
  418. {
  419. title: '上下文字段',
  420. fields: [
  421. { key: 'retry.is_retry', label: '是否重试', tip: 'true 表示重试请求' },
  422. { key: 'last_error.code', label: '上次错误码', tip: '配合重试策略使用' },
  423. {
  424. key: 'metadata.conversation_id',
  425. label: '会话 ID',
  426. tip: '可用于路由或缓存命中',
  427. },
  428. ],
  429. },
  430. {
  431. title: '请求头映射字段',
  432. fields: [
  433. {
  434. key: 'header_override_normalized.authorization',
  435. label: '标准化 Authorization',
  436. tip: '统一小写后可稳定匹配',
  437. },
  438. {
  439. key: 'header_override_normalized.x_debug_mode',
  440. label: '标准化 X-Debug-Mode',
  441. tip: '适合灰度 / 调试开关判断',
  442. },
  443. ],
  444. },
  445. ];
  446. const OPERATION_MODE_LABEL_MAP = OPERATION_MODE_OPTIONS.reduce((acc, item) => {
  447. acc[item.value] = item.label;
  448. return acc;
  449. }, {});
  450. let localIdSeed = 0;
  451. const nextLocalId = () => `param_override_${Date.now()}_${localIdSeed++}`;
  452. const toValueText = (value) => {
  453. if (value === undefined) return '';
  454. if (typeof value === 'string') return value;
  455. try {
  456. return JSON.stringify(value);
  457. } catch (error) {
  458. return String(value);
  459. }
  460. };
  461. const parseLooseValue = (valueText) => {
  462. const raw = String(valueText ?? '');
  463. if (raw.trim() === '') return '';
  464. try {
  465. return JSON.parse(raw);
  466. } catch (error) {
  467. return raw;
  468. }
  469. };
  470. const parsePassHeaderNames = (rawValue) => {
  471. if (Array.isArray(rawValue)) {
  472. return rawValue
  473. .map((item) => String(item ?? '').trim())
  474. .filter(Boolean);
  475. }
  476. if (rawValue && typeof rawValue === 'object') {
  477. if (Array.isArray(rawValue.headers)) {
  478. return rawValue.headers
  479. .map((item) => String(item ?? '').trim())
  480. .filter(Boolean);
  481. }
  482. if (rawValue.header !== undefined) {
  483. const single = String(rawValue.header ?? '').trim();
  484. return single ? [single] : [];
  485. }
  486. return [];
  487. }
  488. if (typeof rawValue === 'string') {
  489. return rawValue
  490. .split(',')
  491. .map((item) => item.trim())
  492. .filter(Boolean);
  493. }
  494. return [];
  495. };
  496. const parseReturnErrorDraft = (valueText) => {
  497. const defaults = {
  498. message: '',
  499. statusCode: 400,
  500. code: '',
  501. type: '',
  502. skipRetry: true,
  503. simpleMode: true,
  504. };
  505. const raw = String(valueText ?? '').trim();
  506. if (!raw) {
  507. return defaults;
  508. }
  509. try {
  510. const parsed = JSON.parse(raw);
  511. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
  512. const statusRaw =
  513. parsed.status_code !== undefined ? parsed.status_code : parsed.status;
  514. const statusValue = Number(statusRaw);
  515. return {
  516. ...defaults,
  517. message: String(parsed.message || parsed.msg || '').trim(),
  518. statusCode:
  519. Number.isInteger(statusValue) &&
  520. statusValue >= 100 &&
  521. statusValue <= 599
  522. ? statusValue
  523. : 400,
  524. code: String(parsed.code || '').trim(),
  525. type: String(parsed.type || '').trim(),
  526. skipRetry: parsed.skip_retry !== false,
  527. simpleMode: false,
  528. };
  529. }
  530. } catch (error) {
  531. // treat as plain text message
  532. }
  533. return {
  534. ...defaults,
  535. message: raw,
  536. simpleMode: true,
  537. };
  538. };
  539. const buildReturnErrorValueText = (draft = {}) => {
  540. const message = String(draft.message || '').trim();
  541. if (draft.simpleMode) {
  542. return message;
  543. }
  544. const statusCode = Number(draft.statusCode);
  545. const payload = {
  546. message,
  547. status_code:
  548. Number.isInteger(statusCode) && statusCode >= 100 && statusCode <= 599
  549. ? statusCode
  550. : 400,
  551. };
  552. const code = String(draft.code || '').trim();
  553. const type = String(draft.type || '').trim();
  554. if (code) payload.code = code;
  555. if (type) payload.type = type;
  556. if (draft.skipRetry === false) {
  557. payload.skip_retry = false;
  558. }
  559. return JSON.stringify(payload);
  560. };
  561. const normalizePruneRule = (rule = {}) => ({
  562. id: nextLocalId(),
  563. path: typeof rule.path === 'string' ? rule.path : '',
  564. mode: CONDITION_MODE_VALUES.has(rule.mode) ? rule.mode : 'full',
  565. value_text: toValueText(rule.value),
  566. invert: rule.invert === true,
  567. pass_missing_key: rule.pass_missing_key === true,
  568. });
  569. const parsePruneObjectsDraft = (valueText) => {
  570. const defaults = {
  571. simpleMode: true,
  572. typeText: '',
  573. logic: 'AND',
  574. recursive: true,
  575. rules: [],
  576. };
  577. const raw = String(valueText ?? '').trim();
  578. if (!raw) {
  579. return defaults;
  580. }
  581. try {
  582. const parsed = JSON.parse(raw);
  583. if (typeof parsed === 'string') {
  584. return {
  585. ...defaults,
  586. simpleMode: true,
  587. typeText: parsed.trim(),
  588. };
  589. }
  590. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
  591. const rules = [];
  592. if (parsed.where && typeof parsed.where === 'object' && !Array.isArray(parsed.where)) {
  593. Object.entries(parsed.where).forEach(([path, value]) => {
  594. rules.push(
  595. normalizePruneRule({
  596. path,
  597. mode: 'full',
  598. value,
  599. }),
  600. );
  601. });
  602. }
  603. if (Array.isArray(parsed.conditions)) {
  604. parsed.conditions.forEach((item) => {
  605. if (item && typeof item === 'object') {
  606. rules.push(normalizePruneRule(item));
  607. }
  608. });
  609. } else if (
  610. parsed.conditions &&
  611. typeof parsed.conditions === 'object' &&
  612. !Array.isArray(parsed.conditions)
  613. ) {
  614. Object.entries(parsed.conditions).forEach(([path, value]) => {
  615. rules.push(
  616. normalizePruneRule({
  617. path,
  618. mode: 'full',
  619. value,
  620. }),
  621. );
  622. });
  623. }
  624. const typeText =
  625. parsed.type === undefined ? '' : String(parsed.type).trim();
  626. const logic =
  627. String(parsed.logic || 'AND').toUpperCase() === 'OR' ? 'OR' : 'AND';
  628. const recursive = parsed.recursive !== false;
  629. const hasAdvancedFields =
  630. parsed.logic !== undefined ||
  631. parsed.recursive !== undefined ||
  632. parsed.where !== undefined ||
  633. parsed.conditions !== undefined;
  634. return {
  635. ...defaults,
  636. simpleMode: !hasAdvancedFields,
  637. typeText,
  638. logic,
  639. recursive,
  640. rules,
  641. };
  642. }
  643. return {
  644. ...defaults,
  645. simpleMode: true,
  646. typeText: String(parsed ?? '').trim(),
  647. };
  648. } catch (error) {
  649. return {
  650. ...defaults,
  651. simpleMode: true,
  652. typeText: raw,
  653. };
  654. }
  655. };
  656. const buildPruneObjectsValueText = (draft = {}) => {
  657. const typeText = String(draft.typeText || '').trim();
  658. if (draft.simpleMode) {
  659. return typeText;
  660. }
  661. const payload = {};
  662. if (typeText) {
  663. payload.type = typeText;
  664. }
  665. if (String(draft.logic || 'AND').toUpperCase() === 'OR') {
  666. payload.logic = 'OR';
  667. }
  668. if (draft.recursive === false) {
  669. payload.recursive = false;
  670. }
  671. const conditions = (draft.rules || [])
  672. .filter((rule) => String(rule.path || '').trim())
  673. .map((rule) => {
  674. const conditionPayload = {
  675. path: String(rule.path || '').trim(),
  676. mode: CONDITION_MODE_VALUES.has(rule.mode) ? rule.mode : 'full',
  677. };
  678. const valueRaw = String(rule.value_text || '').trim();
  679. if (valueRaw !== '') {
  680. conditionPayload.value = parseLooseValue(valueRaw);
  681. }
  682. if (rule.invert) {
  683. conditionPayload.invert = true;
  684. }
  685. if (rule.pass_missing_key) {
  686. conditionPayload.pass_missing_key = true;
  687. }
  688. return conditionPayload;
  689. });
  690. if (conditions.length > 0) {
  691. payload.conditions = conditions;
  692. }
  693. if (!payload.type && !payload.conditions) {
  694. return JSON.stringify({ logic: 'AND' });
  695. }
  696. return JSON.stringify(payload);
  697. };
  698. const parseSyncTargetSpec = (spec) => {
  699. const raw = String(spec ?? '').trim();
  700. if (!raw) return { type: 'json', key: '' };
  701. const idx = raw.indexOf(':');
  702. if (idx < 0) return { type: 'json', key: raw };
  703. const prefix = raw.slice(0, idx).trim().toLowerCase();
  704. const key = raw.slice(idx + 1).trim();
  705. if (prefix === 'header') {
  706. return { type: 'header', key };
  707. }
  708. return { type: 'json', key };
  709. };
  710. const buildSyncTargetSpec = (type, key) => {
  711. const normalizedType = type === 'header' ? 'header' : 'json';
  712. const normalizedKey = String(key ?? '').trim();
  713. if (!normalizedKey) return '';
  714. return `${normalizedType}:${normalizedKey}`;
  715. };
  716. const normalizeCondition = (condition = {}) => ({
  717. id: nextLocalId(),
  718. path: typeof condition.path === 'string' ? condition.path : '',
  719. mode: CONDITION_MODE_VALUES.has(condition.mode) ? condition.mode : 'full',
  720. value_text: toValueText(condition.value),
  721. invert: condition.invert === true,
  722. pass_missing_key: condition.pass_missing_key === true,
  723. });
  724. const createDefaultCondition = () => normalizeCondition({});
  725. const normalizeOperation = (operation = {}) => ({
  726. id: nextLocalId(),
  727. description: typeof operation.description === 'string' ? operation.description : '',
  728. path: typeof operation.path === 'string' ? operation.path : '',
  729. mode: OPERATION_MODE_VALUES.has(operation.mode) ? operation.mode : 'set',
  730. value_text: toValueText(operation.value),
  731. keep_origin: operation.keep_origin === true,
  732. from: typeof operation.from === 'string' ? operation.from : '',
  733. to: typeof operation.to === 'string' ? operation.to : '',
  734. logic: String(operation.logic || 'OR').toUpperCase() === 'AND' ? 'AND' : 'OR',
  735. conditions: Array.isArray(operation.conditions)
  736. ? operation.conditions.map(normalizeCondition)
  737. : [],
  738. });
  739. const createDefaultOperation = () => normalizeOperation({ mode: 'set' });
  740. const getOperationSummary = (operation = {}, index = 0) => {
  741. const mode = operation.mode || 'set';
  742. const modeLabel = OPERATION_MODE_LABEL_MAP[mode] || mode;
  743. if (mode === 'sync_fields') {
  744. const from = String(operation.from || '').trim();
  745. const to = String(operation.to || '').trim();
  746. return `${index + 1}. ${modeLabel} · ${from || to || '-'}`;
  747. }
  748. const path = String(operation.path || '').trim();
  749. const from = String(operation.from || '').trim();
  750. const to = String(operation.to || '').trim();
  751. return `${index + 1}. ${modeLabel} · ${path || from || to || '-'}`;
  752. };
  753. const getOperationModeTagColor = (mode = 'set') => {
  754. if (mode.includes('header')) return 'cyan';
  755. if (mode.includes('replace') || mode.includes('trim')) return 'violet';
  756. if (mode.includes('copy') || mode.includes('move')) return 'blue';
  757. if (mode.includes('error') || mode.includes('prune')) return 'red';
  758. if (mode.includes('sync')) return 'green';
  759. return 'grey';
  760. };
  761. const parseInitialState = (rawValue) => {
  762. const text = typeof rawValue === 'string' ? rawValue : '';
  763. const trimmed = text.trim();
  764. if (!trimmed) {
  765. return {
  766. editMode: 'visual',
  767. visualMode: 'operations',
  768. legacyValue: '',
  769. operations: [createDefaultOperation()],
  770. jsonText: '',
  771. jsonError: '',
  772. };
  773. }
  774. if (!verifyJSON(trimmed)) {
  775. return {
  776. editMode: 'json',
  777. visualMode: 'operations',
  778. legacyValue: '',
  779. operations: [createDefaultOperation()],
  780. jsonText: text,
  781. jsonError: 'JSON 格式不正确',
  782. };
  783. }
  784. const parsed = JSON.parse(trimmed);
  785. const pretty = JSON.stringify(parsed, null, 2);
  786. if (
  787. parsed &&
  788. typeof parsed === 'object' &&
  789. !Array.isArray(parsed) &&
  790. Array.isArray(parsed.operations)
  791. ) {
  792. return {
  793. editMode: 'visual',
  794. visualMode: 'operations',
  795. legacyValue: '',
  796. operations:
  797. parsed.operations.length > 0
  798. ? parsed.operations.map(normalizeOperation)
  799. : [createDefaultOperation()],
  800. jsonText: pretty,
  801. jsonError: '',
  802. };
  803. }
  804. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
  805. return {
  806. editMode: 'visual',
  807. visualMode: 'legacy',
  808. legacyValue: pretty,
  809. operations: [createDefaultOperation()],
  810. jsonText: pretty,
  811. jsonError: '',
  812. };
  813. }
  814. return {
  815. editMode: 'json',
  816. visualMode: 'operations',
  817. legacyValue: '',
  818. operations: [createDefaultOperation()],
  819. jsonText: pretty,
  820. jsonError: '',
  821. };
  822. };
  823. const isOperationBlank = (operation) => {
  824. const hasCondition = (operation.conditions || []).some(
  825. (condition) =>
  826. condition.path.trim() ||
  827. String(condition.value_text ?? '').trim() ||
  828. condition.mode !== 'full' ||
  829. condition.invert ||
  830. condition.pass_missing_key,
  831. );
  832. return (
  833. operation.mode === 'set' &&
  834. !operation.path.trim() &&
  835. !operation.from.trim() &&
  836. !operation.to.trim() &&
  837. String(operation.value_text ?? '').trim() === '' &&
  838. !operation.keep_origin &&
  839. !hasCondition
  840. );
  841. };
  842. const buildConditionPayload = (condition) => {
  843. const path = condition.path.trim();
  844. if (!path) return null;
  845. const payload = {
  846. path,
  847. mode: condition.mode || 'full',
  848. value: parseLooseValue(condition.value_text),
  849. };
  850. if (condition.invert) payload.invert = true;
  851. if (condition.pass_missing_key) payload.pass_missing_key = true;
  852. return payload;
  853. };
  854. const validateOperations = (operations, t) => {
  855. for (let i = 0; i < operations.length; i++) {
  856. const op = operations[i];
  857. const mode = op.mode || 'set';
  858. const meta = MODE_META[mode] || MODE_META.set;
  859. const line = i + 1;
  860. const pathValue = op.path.trim();
  861. const fromValue = op.from.trim();
  862. const toValue = op.to.trim();
  863. if (meta.path && !pathValue) {
  864. return t('第 {{line}} 条操作缺少目标路径', { line });
  865. }
  866. if (FROM_REQUIRED_MODES.has(mode) && !fromValue) {
  867. if (!(meta.pathAlias && pathValue)) {
  868. return t('第 {{line}} 条操作缺少来源字段', { line });
  869. }
  870. }
  871. if (TO_REQUIRED_MODES.has(mode) && !toValue) {
  872. if (!(meta.pathAlias && pathValue)) {
  873. return t('第 {{line}} 条操作缺少目标字段', { line });
  874. }
  875. }
  876. if (meta.from && !fromValue) {
  877. return t('第 {{line}} 条操作缺少来源字段', { line });
  878. }
  879. if (meta.to && !toValue) {
  880. return t('第 {{line}} 条操作缺少目标字段', { line });
  881. }
  882. if (
  883. VALUE_REQUIRED_MODES.has(mode) &&
  884. String(op.value_text ?? '').trim() === ''
  885. ) {
  886. return t('第 {{line}} 条操作缺少值', { line });
  887. }
  888. if (mode === 'return_error') {
  889. const raw = String(op.value_text ?? '').trim();
  890. if (!raw) {
  891. return t('第 {{line}} 条操作缺少值', { line });
  892. }
  893. try {
  894. const parsed = JSON.parse(raw);
  895. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
  896. if (!String(parsed.message || '').trim()) {
  897. return t('第 {{line}} 条 return_error 需要 message 字段', { line });
  898. }
  899. }
  900. } catch (error) {
  901. // plain string value is allowed
  902. }
  903. }
  904. if (mode === 'prune_objects') {
  905. const raw = String(op.value_text ?? '').trim();
  906. if (!raw) {
  907. return t('第 {{line}} 条 prune_objects 缺少条件', { line });
  908. }
  909. try {
  910. const parsed = JSON.parse(raw);
  911. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
  912. const hasType =
  913. parsed.type !== undefined &&
  914. String(parsed.type).trim() !== '';
  915. const hasWhere =
  916. parsed.where &&
  917. typeof parsed.where === 'object' &&
  918. !Array.isArray(parsed.where) &&
  919. Object.keys(parsed.where).length > 0;
  920. const hasConditionsArray =
  921. Array.isArray(parsed.conditions) && parsed.conditions.length > 0;
  922. const hasConditionsObject =
  923. parsed.conditions &&
  924. typeof parsed.conditions === 'object' &&
  925. !Array.isArray(parsed.conditions) &&
  926. Object.keys(parsed.conditions).length > 0;
  927. if (!hasType && !hasWhere && !hasConditionsArray && !hasConditionsObject) {
  928. return t('第 {{line}} 条 prune_objects 需要至少一个匹配条件', {
  929. line,
  930. });
  931. }
  932. }
  933. } catch (error) {
  934. // non-JSON string is treated as type string
  935. }
  936. }
  937. if (mode === 'pass_headers') {
  938. const raw = String(op.value_text ?? '').trim();
  939. if (!raw) {
  940. return t('第 {{line}} 条请求头透传缺少请求头名称', { line });
  941. }
  942. const parsed = parseLooseValue(raw);
  943. const headers = parsePassHeaderNames(parsed);
  944. if (headers.length === 0) {
  945. return t('第 {{line}} 条请求头透传格式无效', { line });
  946. }
  947. }
  948. }
  949. return '';
  950. };
  951. const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
  952. const { t } = useTranslation();
  953. const [editMode, setEditMode] = useState('visual');
  954. const [visualMode, setVisualMode] = useState('operations');
  955. const [legacyValue, setLegacyValue] = useState('');
  956. const [operations, setOperations] = useState([createDefaultOperation()]);
  957. const [jsonText, setJsonText] = useState('');
  958. const [jsonError, setJsonError] = useState('');
  959. const [operationSearch, setOperationSearch] = useState('');
  960. const [selectedOperationId, setSelectedOperationId] = useState('');
  961. const [expandedConditionMap, setExpandedConditionMap] = useState({});
  962. const [templateGroupKey, setTemplateGroupKey] = useState('basic');
  963. const [templatePresetKey, setTemplatePresetKey] = useState('operations_default');
  964. const [fieldGuideVisible, setFieldGuideVisible] = useState(false);
  965. const [fieldGuideTarget, setFieldGuideTarget] = useState('path');
  966. const [fieldGuideKeyword, setFieldGuideKeyword] = useState('');
  967. useEffect(() => {
  968. if (!visible) return;
  969. const nextState = parseInitialState(value);
  970. setEditMode(nextState.editMode);
  971. setVisualMode(nextState.visualMode);
  972. setLegacyValue(nextState.legacyValue);
  973. setOperations(nextState.operations);
  974. setJsonText(nextState.jsonText);
  975. setJsonError(nextState.jsonError);
  976. setOperationSearch('');
  977. setSelectedOperationId(nextState.operations[0]?.id || '');
  978. setExpandedConditionMap({});
  979. if (nextState.visualMode === 'legacy') {
  980. setTemplateGroupKey('basic');
  981. setTemplatePresetKey('legacy_default');
  982. } else {
  983. setTemplateGroupKey('basic');
  984. setTemplatePresetKey('operations_default');
  985. }
  986. setFieldGuideVisible(false);
  987. setFieldGuideTarget('path');
  988. setFieldGuideKeyword('');
  989. }, [visible, value]);
  990. useEffect(() => {
  991. if (operations.length === 0) {
  992. setSelectedOperationId('');
  993. return;
  994. }
  995. if (!operations.some((item) => item.id === selectedOperationId)) {
  996. setSelectedOperationId(operations[0].id);
  997. }
  998. }, [operations, selectedOperationId]);
  999. const templatePresetOptions = useMemo(
  1000. () =>
  1001. Object.entries(TEMPLATE_PRESET_CONFIG)
  1002. .filter(([, config]) => config.group === templateGroupKey)
  1003. .map(([value, config]) => ({
  1004. value,
  1005. label: config.label,
  1006. })),
  1007. [templateGroupKey],
  1008. );
  1009. useEffect(() => {
  1010. if (templatePresetOptions.length === 0) return;
  1011. const exists = templatePresetOptions.some(
  1012. (item) => item.value === templatePresetKey,
  1013. );
  1014. if (!exists) {
  1015. setTemplatePresetKey(templatePresetOptions[0].value);
  1016. }
  1017. }, [templatePresetKey, templatePresetOptions]);
  1018. const operationCount = useMemo(
  1019. () => operations.filter((item) => !isOperationBlank(item)).length,
  1020. [operations],
  1021. );
  1022. const filteredOperations = useMemo(() => {
  1023. const keyword = operationSearch.trim().toLowerCase();
  1024. if (!keyword) return operations;
  1025. return operations.filter((operation) => {
  1026. const searchableText = [
  1027. operation.description,
  1028. operation.mode,
  1029. operation.path,
  1030. operation.from,
  1031. operation.to,
  1032. operation.value_text,
  1033. ]
  1034. .filter(Boolean)
  1035. .join(' ')
  1036. .toLowerCase();
  1037. return searchableText.includes(keyword);
  1038. });
  1039. }, [operationSearch, operations]);
  1040. const selectedOperation = useMemo(
  1041. () => operations.find((operation) => operation.id === selectedOperationId),
  1042. [operations, selectedOperationId],
  1043. );
  1044. const selectedOperationIndex = useMemo(
  1045. () =>
  1046. operations.findIndex((operation) => operation.id === selectedOperationId),
  1047. [operations, selectedOperationId],
  1048. );
  1049. const returnErrorDraft = useMemo(() => {
  1050. if (!selectedOperation || (selectedOperation.mode || '') !== 'return_error') {
  1051. return null;
  1052. }
  1053. return parseReturnErrorDraft(selectedOperation.value_text);
  1054. }, [selectedOperation]);
  1055. const pruneObjectsDraft = useMemo(() => {
  1056. if (!selectedOperation || (selectedOperation.mode || '') !== 'prune_objects') {
  1057. return null;
  1058. }
  1059. return parsePruneObjectsDraft(selectedOperation.value_text);
  1060. }, [selectedOperation]);
  1061. const topOperationModes = useMemo(() => {
  1062. const counts = operations.reduce((acc, operation) => {
  1063. const mode = operation.mode || 'set';
  1064. acc[mode] = (acc[mode] || 0) + 1;
  1065. return acc;
  1066. }, {});
  1067. return Object.entries(counts)
  1068. .sort((a, b) => b[1] - a[1])
  1069. .slice(0, 4);
  1070. }, [operations]);
  1071. const buildOperationsJson = useCallback(
  1072. (sourceOperations, options = {}) => {
  1073. const { validate = true } = options;
  1074. const filteredOps = sourceOperations.filter((item) => !isOperationBlank(item));
  1075. if (filteredOps.length === 0) return '';
  1076. if (validate) {
  1077. const message = validateOperations(filteredOps, t);
  1078. if (message) {
  1079. throw new Error(message);
  1080. }
  1081. }
  1082. const payloadOps = filteredOps.map((operation) => {
  1083. const mode = operation.mode || 'set';
  1084. const meta = MODE_META[mode] || MODE_META.set;
  1085. const descriptionValue = String(operation.description || '').trim();
  1086. const pathValue = operation.path.trim();
  1087. const fromValue = operation.from.trim();
  1088. const toValue = operation.to.trim();
  1089. const payload = { mode };
  1090. if (descriptionValue) {
  1091. payload.description = descriptionValue;
  1092. }
  1093. if (meta.path) {
  1094. payload.path = pathValue;
  1095. }
  1096. if (meta.pathOptional && pathValue) {
  1097. payload.path = pathValue;
  1098. }
  1099. if (meta.value) {
  1100. payload.value = parseLooseValue(operation.value_text);
  1101. }
  1102. if (meta.keepOrigin && operation.keep_origin) {
  1103. payload.keep_origin = true;
  1104. }
  1105. if (meta.from) {
  1106. payload.from = fromValue;
  1107. }
  1108. if (!meta.to && operation.to.trim()) {
  1109. payload.to = toValue;
  1110. }
  1111. if (meta.to) {
  1112. payload.to = toValue;
  1113. }
  1114. if (meta.pathAlias) {
  1115. if (!payload.from && pathValue) {
  1116. payload.from = pathValue;
  1117. }
  1118. if (!payload.to && pathValue) {
  1119. payload.to = pathValue;
  1120. }
  1121. }
  1122. const conditions = (operation.conditions || [])
  1123. .map(buildConditionPayload)
  1124. .filter(Boolean);
  1125. if (conditions.length > 0) {
  1126. payload.conditions = conditions;
  1127. payload.logic = operation.logic === 'AND' ? 'AND' : 'OR';
  1128. }
  1129. return payload;
  1130. });
  1131. return JSON.stringify({ operations: payloadOps }, null, 2);
  1132. },
  1133. [t],
  1134. );
  1135. const buildVisualJson = useCallback(() => {
  1136. if (visualMode === 'legacy') {
  1137. const trimmed = legacyValue.trim();
  1138. if (!trimmed) return '';
  1139. if (!verifyJSON(trimmed)) {
  1140. throw new Error(t('参数覆盖必须是合法的 JSON 格式!'));
  1141. }
  1142. const parsed = JSON.parse(trimmed);
  1143. if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
  1144. throw new Error(t('旧格式必须是 JSON 对象'));
  1145. }
  1146. return JSON.stringify(parsed, null, 2);
  1147. }
  1148. return buildOperationsJson(operations, { validate: true });
  1149. }, [buildOperationsJson, legacyValue, operations, t, visualMode]);
  1150. const switchToJsonMode = () => {
  1151. if (editMode === 'json') return;
  1152. try {
  1153. setJsonText(buildVisualJson());
  1154. setJsonError('');
  1155. } catch (error) {
  1156. showError(error.message);
  1157. if (visualMode === 'legacy') {
  1158. setJsonText(legacyValue);
  1159. } else {
  1160. setJsonText(buildOperationsJson(operations, { validate: false }));
  1161. }
  1162. setJsonError(error.message || t('参数配置有误'));
  1163. }
  1164. setEditMode('json');
  1165. };
  1166. const switchToVisualMode = () => {
  1167. if (editMode === 'visual') return;
  1168. const trimmed = jsonText.trim();
  1169. if (!trimmed) {
  1170. const fallback = createDefaultOperation();
  1171. setVisualMode('operations');
  1172. setOperations([fallback]);
  1173. setSelectedOperationId(fallback.id);
  1174. setLegacyValue('');
  1175. setJsonError('');
  1176. setEditMode('visual');
  1177. return;
  1178. }
  1179. if (!verifyJSON(trimmed)) {
  1180. showError(t('参数覆盖必须是合法的 JSON 格式!'));
  1181. return;
  1182. }
  1183. const parsed = JSON.parse(trimmed);
  1184. if (
  1185. parsed &&
  1186. typeof parsed === 'object' &&
  1187. !Array.isArray(parsed) &&
  1188. Array.isArray(parsed.operations)
  1189. ) {
  1190. const nextOperations =
  1191. parsed.operations.length > 0
  1192. ? parsed.operations.map(normalizeOperation)
  1193. : [createDefaultOperation()];
  1194. setVisualMode('operations');
  1195. setOperations(nextOperations);
  1196. setSelectedOperationId(nextOperations[0]?.id || '');
  1197. setLegacyValue('');
  1198. setJsonError('');
  1199. setEditMode('visual');
  1200. setTemplateGroupKey('basic');
  1201. setTemplatePresetKey('operations_default');
  1202. return;
  1203. }
  1204. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
  1205. const fallback = createDefaultOperation();
  1206. setVisualMode('legacy');
  1207. setLegacyValue(JSON.stringify(parsed, null, 2));
  1208. setOperations([fallback]);
  1209. setSelectedOperationId(fallback.id);
  1210. setJsonError('');
  1211. setEditMode('visual');
  1212. setTemplateGroupKey('basic');
  1213. setTemplatePresetKey('legacy_default');
  1214. return;
  1215. }
  1216. showError(t('参数覆盖必须是合法的 JSON 对象'));
  1217. };
  1218. const fillLegacyTemplate = (legacyPayload) => {
  1219. const text = JSON.stringify(legacyPayload, null, 2);
  1220. const fallback = createDefaultOperation();
  1221. setVisualMode('legacy');
  1222. setLegacyValue(text);
  1223. setOperations([fallback]);
  1224. setSelectedOperationId(fallback.id);
  1225. setExpandedConditionMap({});
  1226. setJsonText(text);
  1227. setJsonError('');
  1228. setEditMode('visual');
  1229. };
  1230. const fillOperationsTemplate = (operationsPayload) => {
  1231. const nextOperations = (operationsPayload || []).map(normalizeOperation);
  1232. const finalOperations =
  1233. nextOperations.length > 0 ? nextOperations : [createDefaultOperation()];
  1234. setVisualMode('operations');
  1235. setOperations(finalOperations);
  1236. setSelectedOperationId(finalOperations[0]?.id || '');
  1237. setExpandedConditionMap({});
  1238. setJsonText(JSON.stringify({ operations: operationsPayload || [] }, null, 2));
  1239. setJsonError('');
  1240. setEditMode('visual');
  1241. };
  1242. const appendLegacyTemplate = (legacyPayload) => {
  1243. let parsedCurrent = {};
  1244. if (visualMode === 'legacy') {
  1245. const trimmed = legacyValue.trim();
  1246. if (trimmed) {
  1247. if (!verifyJSON(trimmed)) {
  1248. showError(t('当前旧格式 JSON 不合法,无法追加模板'));
  1249. return;
  1250. }
  1251. const parsed = JSON.parse(trimmed);
  1252. if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
  1253. showError(t('当前旧格式不是 JSON 对象,无法追加模板'));
  1254. return;
  1255. }
  1256. parsedCurrent = parsed;
  1257. }
  1258. }
  1259. const merged = {
  1260. ...(legacyPayload || {}),
  1261. ...parsedCurrent,
  1262. };
  1263. const text = JSON.stringify(merged, null, 2);
  1264. const fallback = createDefaultOperation();
  1265. setVisualMode('legacy');
  1266. setLegacyValue(text);
  1267. setOperations([fallback]);
  1268. setSelectedOperationId(fallback.id);
  1269. setExpandedConditionMap({});
  1270. setJsonText(text);
  1271. setJsonError('');
  1272. setEditMode('visual');
  1273. };
  1274. const appendOperationsTemplate = (operationsPayload) => {
  1275. const appended = (operationsPayload || []).map(normalizeOperation);
  1276. const existing =
  1277. visualMode === 'operations'
  1278. ? operations.filter((item) => !isOperationBlank(item))
  1279. : [];
  1280. const nextOperations = [...existing, ...appended];
  1281. setVisualMode('operations');
  1282. setOperations(nextOperations.length > 0 ? nextOperations : appended);
  1283. setSelectedOperationId(nextOperations[0]?.id || appended[0]?.id || '');
  1284. setExpandedConditionMap({});
  1285. setLegacyValue('');
  1286. setJsonError('');
  1287. setEditMode('visual');
  1288. setJsonText('');
  1289. };
  1290. const clearValue = () => {
  1291. const fallback = createDefaultOperation();
  1292. setVisualMode('operations');
  1293. setLegacyValue('');
  1294. setOperations([fallback]);
  1295. setSelectedOperationId(fallback.id);
  1296. setExpandedConditionMap({});
  1297. setJsonText('');
  1298. setJsonError('');
  1299. setTemplateGroupKey('basic');
  1300. setTemplatePresetKey('operations_default');
  1301. };
  1302. const getSelectedTemplatePreset = () =>
  1303. TEMPLATE_PRESET_CONFIG[templatePresetKey] ||
  1304. TEMPLATE_PRESET_CONFIG.operations_default;
  1305. const fillTemplateFromLibrary = () => {
  1306. const preset = getSelectedTemplatePreset();
  1307. if (preset.kind === 'legacy') {
  1308. fillLegacyTemplate(preset.payload || {});
  1309. return;
  1310. }
  1311. fillOperationsTemplate(preset.payload?.operations || []);
  1312. };
  1313. const appendTemplateFromLibrary = () => {
  1314. const preset = getSelectedTemplatePreset();
  1315. if (preset.kind === 'legacy') {
  1316. appendLegacyTemplate(preset.payload || {});
  1317. return;
  1318. }
  1319. appendOperationsTemplate(preset.payload?.operations || []);
  1320. };
  1321. const resetEditorState = () => {
  1322. clearValue();
  1323. setEditMode('visual');
  1324. };
  1325. const applyBuiltinField = (fieldKey, target = 'path') => {
  1326. if (!selectedOperation) {
  1327. showError(t('请先选择一条规则'));
  1328. return;
  1329. }
  1330. const mode = selectedOperation.mode || 'set';
  1331. const meta = MODE_META[mode] || MODE_META.set;
  1332. if (target === 'path' && (meta.path || meta.pathOptional || meta.pathAlias)) {
  1333. updateOperation(selectedOperation.id, { path: fieldKey });
  1334. return;
  1335. }
  1336. if (target === 'from' && (meta.from || meta.pathAlias || mode === 'sync_fields')) {
  1337. updateOperation(selectedOperation.id, {
  1338. from: mode === 'sync_fields' ? buildSyncTargetSpec('json', fieldKey) : fieldKey,
  1339. });
  1340. return;
  1341. }
  1342. if (target === 'to' && (meta.to || mode === 'sync_fields')) {
  1343. updateOperation(selectedOperation.id, {
  1344. to: mode === 'sync_fields' ? buildSyncTargetSpec('json', fieldKey) : fieldKey,
  1345. });
  1346. return;
  1347. }
  1348. showError(t('当前规则不支持写入到该位置'));
  1349. };
  1350. const openFieldGuide = (target = 'path') => {
  1351. setFieldGuideTarget(target);
  1352. setFieldGuideVisible(true);
  1353. };
  1354. const copyBuiltinField = async (fieldKey) => {
  1355. const ok = await copy(fieldKey);
  1356. if (ok) {
  1357. showSuccess(t('已复制字段:{{name}}', { name: fieldKey }));
  1358. } else {
  1359. showError(t('复制失败'));
  1360. }
  1361. };
  1362. const filteredFieldGuideSections = useMemo(() => {
  1363. const keyword = fieldGuideKeyword.trim().toLowerCase();
  1364. if (!keyword) {
  1365. return BUILTIN_FIELD_SECTIONS;
  1366. }
  1367. return BUILTIN_FIELD_SECTIONS.map((section) => ({
  1368. ...section,
  1369. fields: section.fields.filter((field) =>
  1370. [field.key, field.label, field.tip]
  1371. .filter(Boolean)
  1372. .join(' ')
  1373. .toLowerCase()
  1374. .includes(keyword),
  1375. ),
  1376. })).filter((section) => section.fields.length > 0);
  1377. }, [fieldGuideKeyword]);
  1378. const fieldGuideActionLabel = useMemo(() => {
  1379. if (fieldGuideTarget === 'from') return t('填入来源');
  1380. if (fieldGuideTarget === 'to') return t('填入目标');
  1381. return t('填入路径');
  1382. }, [fieldGuideTarget, t]);
  1383. const fieldGuideFieldCount = useMemo(
  1384. () =>
  1385. filteredFieldGuideSections.reduce(
  1386. (total, section) => total + section.fields.length,
  1387. 0,
  1388. ),
  1389. [filteredFieldGuideSections],
  1390. );
  1391. const updateOperation = (operationId, patch) => {
  1392. setOperations((prev) =>
  1393. prev.map((item) =>
  1394. item.id === operationId ? { ...item, ...patch } : item,
  1395. ),
  1396. );
  1397. };
  1398. const formatSelectedOperationValueAsJson = useCallback(() => {
  1399. if (!selectedOperation) return;
  1400. const raw = String(selectedOperation.value_text || '').trim();
  1401. if (!raw) return;
  1402. if (!verifyJSON(raw)) {
  1403. showError(t('当前值不是合法 JSON,无法格式化'));
  1404. return;
  1405. }
  1406. try {
  1407. updateOperation(selectedOperation.id, {
  1408. value_text: JSON.stringify(JSON.parse(raw), null, 2),
  1409. });
  1410. showSuccess(t('JSON 已格式化'));
  1411. } catch (error) {
  1412. showError(t('当前值不是合法 JSON,无法格式化'));
  1413. }
  1414. }, [selectedOperation, t, updateOperation]);
  1415. const updateReturnErrorDraft = (operationId, draftPatch = {}) => {
  1416. const current = operations.find((item) => item.id === operationId);
  1417. if (!current) return;
  1418. const draft = parseReturnErrorDraft(current.value_text);
  1419. const nextDraft = { ...draft, ...draftPatch };
  1420. updateOperation(operationId, {
  1421. value_text: buildReturnErrorValueText(nextDraft),
  1422. });
  1423. };
  1424. const updatePruneObjectsDraft = (operationId, updater) => {
  1425. const current = operations.find((item) => item.id === operationId);
  1426. if (!current) return;
  1427. const draft = parsePruneObjectsDraft(current.value_text);
  1428. const nextDraft =
  1429. typeof updater === 'function'
  1430. ? updater(draft)
  1431. : { ...draft, ...(updater || {}) };
  1432. updateOperation(operationId, {
  1433. value_text: buildPruneObjectsValueText(nextDraft),
  1434. });
  1435. };
  1436. const addPruneRule = (operationId) => {
  1437. updatePruneObjectsDraft(operationId, (draft) => ({
  1438. ...draft,
  1439. simpleMode: false,
  1440. rules: [...(draft.rules || []), normalizePruneRule({})],
  1441. }));
  1442. };
  1443. const updatePruneRule = (operationId, ruleId, patch) => {
  1444. updatePruneObjectsDraft(operationId, (draft) => ({
  1445. ...draft,
  1446. rules: (draft.rules || []).map((rule) =>
  1447. rule.id === ruleId ? { ...rule, ...patch } : rule,
  1448. ),
  1449. }));
  1450. };
  1451. const removePruneRule = (operationId, ruleId) => {
  1452. updatePruneObjectsDraft(operationId, (draft) => ({
  1453. ...draft,
  1454. rules: (draft.rules || []).filter((rule) => rule.id !== ruleId),
  1455. }));
  1456. };
  1457. const addOperation = () => {
  1458. const created = createDefaultOperation();
  1459. setOperations((prev) => [...prev, created]);
  1460. setSelectedOperationId(created.id);
  1461. };
  1462. const duplicateOperation = (operationId) => {
  1463. let insertedId = '';
  1464. setOperations((prev) => {
  1465. const index = prev.findIndex((item) => item.id === operationId);
  1466. if (index < 0) return prev;
  1467. const source = prev[index];
  1468. const cloned = normalizeOperation({
  1469. description: source.description,
  1470. path: source.path,
  1471. mode: source.mode,
  1472. value: parseLooseValue(source.value_text),
  1473. keep_origin: source.keep_origin,
  1474. from: source.from,
  1475. to: source.to,
  1476. logic: source.logic,
  1477. conditions: (source.conditions || []).map((condition) => ({
  1478. path: condition.path,
  1479. mode: condition.mode,
  1480. value: parseLooseValue(condition.value_text),
  1481. invert: condition.invert,
  1482. pass_missing_key: condition.pass_missing_key,
  1483. })),
  1484. });
  1485. insertedId = cloned.id;
  1486. const next = [...prev];
  1487. next.splice(index + 1, 0, cloned);
  1488. return next;
  1489. });
  1490. if (insertedId) {
  1491. setSelectedOperationId(insertedId);
  1492. }
  1493. };
  1494. const removeOperation = (operationId) => {
  1495. setOperations((prev) => {
  1496. if (prev.length <= 1) return [createDefaultOperation()];
  1497. return prev.filter((item) => item.id !== operationId);
  1498. });
  1499. setExpandedConditionMap((prev) => {
  1500. if (!Object.prototype.hasOwnProperty.call(prev, operationId)) {
  1501. return prev;
  1502. }
  1503. const next = { ...prev };
  1504. delete next[operationId];
  1505. return next;
  1506. });
  1507. };
  1508. const addCondition = (operationId) => {
  1509. const createdCondition = createDefaultCondition();
  1510. setOperations((prev) =>
  1511. prev.map((operation) =>
  1512. operation.id === operationId
  1513. ? {
  1514. ...operation,
  1515. conditions: [...(operation.conditions || []), createdCondition],
  1516. }
  1517. : operation,
  1518. ),
  1519. );
  1520. setExpandedConditionMap((prev) => ({
  1521. ...prev,
  1522. [operationId]: [...(prev[operationId] || []), createdCondition.id],
  1523. }));
  1524. };
  1525. const updateCondition = (operationId, conditionId, patch) => {
  1526. setOperations((prev) =>
  1527. prev.map((operation) => {
  1528. if (operation.id !== operationId) return operation;
  1529. return {
  1530. ...operation,
  1531. conditions: (operation.conditions || []).map((condition) =>
  1532. condition.id === conditionId
  1533. ? { ...condition, ...patch }
  1534. : condition,
  1535. ),
  1536. };
  1537. }),
  1538. );
  1539. };
  1540. const removeCondition = (operationId, conditionId) => {
  1541. setOperations((prev) =>
  1542. prev.map((operation) => {
  1543. if (operation.id !== operationId) return operation;
  1544. return {
  1545. ...operation,
  1546. conditions: (operation.conditions || []).filter(
  1547. (condition) => condition.id !== conditionId,
  1548. ),
  1549. };
  1550. }),
  1551. );
  1552. setExpandedConditionMap((prev) => ({
  1553. ...prev,
  1554. [operationId]: (prev[operationId] || []).filter(
  1555. (id) => id !== conditionId,
  1556. ),
  1557. }));
  1558. };
  1559. const selectedConditionKeys = useMemo(
  1560. () => expandedConditionMap[selectedOperationId] || [],
  1561. [expandedConditionMap, selectedOperationId],
  1562. );
  1563. const handleConditionCollapseChange = useCallback(
  1564. (operationId, activeKeys) => {
  1565. const keys = (
  1566. Array.isArray(activeKeys) ? activeKeys : [activeKeys]
  1567. ).filter(Boolean);
  1568. setExpandedConditionMap((prev) => ({
  1569. ...prev,
  1570. [operationId]: keys,
  1571. }));
  1572. },
  1573. [],
  1574. );
  1575. const expandAllSelectedConditions = useCallback(() => {
  1576. if (!selectedOperationId || !selectedOperation) return;
  1577. setExpandedConditionMap((prev) => ({
  1578. ...prev,
  1579. [selectedOperationId]: (selectedOperation.conditions || []).map(
  1580. (condition) => condition.id,
  1581. ),
  1582. }));
  1583. }, [selectedOperation, selectedOperationId]);
  1584. const collapseAllSelectedConditions = useCallback(() => {
  1585. if (!selectedOperationId) return;
  1586. setExpandedConditionMap((prev) => ({
  1587. ...prev,
  1588. [selectedOperationId]: [],
  1589. }));
  1590. }, [selectedOperationId]);
  1591. const handleJsonChange = (nextValue) => {
  1592. setJsonText(nextValue);
  1593. const trimmed = String(nextValue || '').trim();
  1594. if (!trimmed) {
  1595. setJsonError('');
  1596. return;
  1597. }
  1598. if (!verifyJSON(trimmed)) {
  1599. setJsonError(t('JSON格式错误'));
  1600. return;
  1601. }
  1602. setJsonError('');
  1603. };
  1604. const formatJson = () => {
  1605. const trimmed = jsonText.trim();
  1606. if (!trimmed) return;
  1607. if (!verifyJSON(trimmed)) {
  1608. showError(t('参数覆盖必须是合法的 JSON 格式!'));
  1609. return;
  1610. }
  1611. setJsonText(JSON.stringify(JSON.parse(trimmed), null, 2));
  1612. setJsonError('');
  1613. };
  1614. const visualValidationError = useMemo(() => {
  1615. if (editMode !== 'visual') {
  1616. return '';
  1617. }
  1618. try {
  1619. buildVisualJson();
  1620. return '';
  1621. } catch (error) {
  1622. return error?.message || t('参数配置有误');
  1623. }
  1624. }, [buildVisualJson, editMode, t]);
  1625. const handleSave = () => {
  1626. try {
  1627. let result = '';
  1628. if (editMode === 'json') {
  1629. const trimmed = jsonText.trim();
  1630. if (!trimmed) {
  1631. result = '';
  1632. } else {
  1633. if (!verifyJSON(trimmed)) {
  1634. throw new Error(t('参数覆盖必须是合法的 JSON 格式!'));
  1635. }
  1636. result = JSON.stringify(JSON.parse(trimmed), null, 2);
  1637. }
  1638. } else {
  1639. result = buildVisualJson();
  1640. }
  1641. onSave?.(result);
  1642. } catch (error) {
  1643. showError(error.message);
  1644. }
  1645. };
  1646. return (
  1647. <>
  1648. <Modal
  1649. title={t('参数覆盖')}
  1650. visible={visible}
  1651. width={1120}
  1652. bodyStyle={{ maxHeight: '76vh', overflowY: 'auto', paddingTop: 10 }}
  1653. onCancel={onCancel}
  1654. onOk={handleSave}
  1655. okText={t('保存')}
  1656. cancelText={t('取消')}
  1657. >
  1658. <Space vertical align='start' spacing={14} style={{ width: '100%' }}>
  1659. <Card
  1660. className='!rounded-xl !border-0 w-full'
  1661. bodyStyle={{
  1662. padding: 12,
  1663. background: 'var(--semi-color-fill-0)',
  1664. }}
  1665. >
  1666. <div className='flex items-start justify-between gap-3'>
  1667. <Space wrap spacing={8}>
  1668. <Tag color='grey'>{t('编辑方式')}</Tag>
  1669. <Button
  1670. type={editMode === 'visual' ? 'primary' : 'tertiary'}
  1671. onClick={switchToVisualMode}
  1672. >
  1673. {t('可视化')}
  1674. </Button>
  1675. <Button
  1676. type={editMode === 'json' ? 'primary' : 'tertiary'}
  1677. onClick={switchToJsonMode}
  1678. >
  1679. {t('JSON 文本')}
  1680. </Button>
  1681. <Tag color='grey'>{t('模板')}</Tag>
  1682. <Select
  1683. value={templateGroupKey}
  1684. optionList={TEMPLATE_GROUP_OPTIONS}
  1685. onChange={(nextValue) =>
  1686. setTemplateGroupKey(nextValue || 'basic')
  1687. }
  1688. style={{ width: 120 }}
  1689. />
  1690. <Select
  1691. value={templatePresetKey}
  1692. optionList={templatePresetOptions}
  1693. onChange={(nextValue) =>
  1694. setTemplatePresetKey(nextValue || 'operations_default')
  1695. }
  1696. style={{ width: 260 }}
  1697. />
  1698. <Button onClick={fillTemplateFromLibrary}>{t('填充模板')}</Button>
  1699. <Button type='tertiary' onClick={appendTemplateFromLibrary}>
  1700. {t('追加模板')}
  1701. </Button>
  1702. <Button type='tertiary' onClick={resetEditorState}>
  1703. {t('重置')}
  1704. </Button>
  1705. </Space>
  1706. <Text
  1707. type='tertiary'
  1708. size='small'
  1709. className='cursor-pointer select-none mt-1 whitespace-nowrap'
  1710. onClick={() => openFieldGuide('path')}
  1711. >
  1712. {t('字段速查')}
  1713. </Text>
  1714. </div>
  1715. </Card>
  1716. {editMode === 'visual' ? (
  1717. <div style={{ width: '100%' }}>
  1718. {visualMode === 'legacy' ? (
  1719. <Card
  1720. className='!rounded-2xl !border-0'
  1721. bodyStyle={{
  1722. padding: 14,
  1723. background: 'var(--semi-color-fill-0)',
  1724. }}
  1725. >
  1726. <Text className='mb-2 block'>{t('旧格式(JSON 对象)')}</Text>
  1727. <TextArea
  1728. value={legacyValue}
  1729. autosize={{ minRows: 10, maxRows: 20 }}
  1730. placeholder={JSON.stringify(LEGACY_TEMPLATE, null, 2)}
  1731. onChange={(nextValue) => setLegacyValue(nextValue)}
  1732. showClear
  1733. />
  1734. <Text type='tertiary' size='small' className='mt-2 block'>
  1735. {t('这里直接编辑 JSON 对象。适合简单覆盖参数的场景。')}
  1736. </Text>
  1737. </Card>
  1738. ) : (
  1739. <div>
  1740. <div className='flex items-center justify-between mb-3'>
  1741. <Space>
  1742. <Text>{t('新格式(规则 + 条件)')}</Text>
  1743. <Tag color='cyan'>{`${t('规则')}: ${operationCount}`}</Tag>
  1744. </Space>
  1745. <Button icon={<IconPlus />} onClick={addOperation}>
  1746. {t('新增规则')}
  1747. </Button>
  1748. </div>
  1749. <Row gutter={12}>
  1750. <Col xs={24} md={8}>
  1751. <Card
  1752. className='!rounded-2xl !border-0 h-full'
  1753. bodyStyle={{
  1754. padding: 12,
  1755. background: 'var(--semi-color-fill-0)',
  1756. display: 'flex',
  1757. flexDirection: 'column',
  1758. gap: 10,
  1759. minHeight: 520,
  1760. }}
  1761. >
  1762. <div className='flex items-center justify-between'>
  1763. <Text strong>{t('规则导航')}</Text>
  1764. <Tag color='grey'>{`${operationCount}/${operations.length}`}</Tag>
  1765. </div>
  1766. {topOperationModes.length > 0 ? (
  1767. <Space wrap spacing={6}>
  1768. {topOperationModes.map(([mode, count]) => (
  1769. <Tag
  1770. key={`mode_stat_${mode}`}
  1771. size='small'
  1772. color={getOperationModeTagColor(mode)}
  1773. >
  1774. {`${OPERATION_MODE_LABEL_MAP[mode] || mode} · ${count}`}
  1775. </Tag>
  1776. ))}
  1777. </Space>
  1778. ) : null}
  1779. <Input
  1780. value={operationSearch}
  1781. placeholder={t('搜索规则(描述 / 类型 / 路径 / 来源 / 目标)')}
  1782. onChange={(nextValue) =>
  1783. setOperationSearch(nextValue || '')
  1784. }
  1785. showClear
  1786. />
  1787. <div
  1788. className='overflow-auto'
  1789. style={{ flex: 1, minHeight: 320, paddingRight: 2 }}
  1790. >
  1791. {filteredOperations.length === 0 ? (
  1792. <Text type='tertiary' size='small'>
  1793. {t('没有匹配的规则')}
  1794. </Text>
  1795. ) : (
  1796. <div
  1797. style={{
  1798. display: 'flex',
  1799. flexDirection: 'column',
  1800. gap: 8,
  1801. width: '100%',
  1802. }}
  1803. >
  1804. {filteredOperations.map((operation) => {
  1805. const index = operations.findIndex(
  1806. (item) => item.id === operation.id,
  1807. );
  1808. const isActive =
  1809. operation.id === selectedOperationId;
  1810. return (
  1811. <div
  1812. key={operation.id}
  1813. role='button'
  1814. tabIndex={0}
  1815. onClick={() =>
  1816. setSelectedOperationId(operation.id)
  1817. }
  1818. onKeyDown={(event) => {
  1819. if (
  1820. event.key === 'Enter' ||
  1821. event.key === ' '
  1822. ) {
  1823. event.preventDefault();
  1824. setSelectedOperationId(operation.id);
  1825. }
  1826. }}
  1827. className='w-full rounded-xl px-3 py-3 cursor-pointer transition-colors'
  1828. style={{
  1829. background: isActive
  1830. ? 'var(--semi-color-primary-light-default)'
  1831. : 'var(--semi-color-bg-2)',
  1832. border: isActive
  1833. ? '1px solid var(--semi-color-primary)'
  1834. : '1px solid var(--semi-color-border)',
  1835. }}
  1836. >
  1837. <div className='flex items-start justify-between gap-2'>
  1838. <div>
  1839. <Text strong>{`#${index + 1}`}</Text>
  1840. <Text
  1841. type='tertiary'
  1842. size='small'
  1843. className='block mt-1'
  1844. >
  1845. {getOperationSummary(operation, index)}
  1846. </Text>
  1847. {String(operation.description || '').trim() ? (
  1848. <Text
  1849. type='tertiary'
  1850. size='small'
  1851. className='block mt-1'
  1852. style={{
  1853. lineHeight: 1.5,
  1854. wordBreak: 'break-word',
  1855. overflow: 'hidden',
  1856. display: '-webkit-box',
  1857. WebkitLineClamp: 2,
  1858. WebkitBoxOrient: 'vertical',
  1859. }}
  1860. >
  1861. {operation.description}
  1862. </Text>
  1863. ) : null}
  1864. </div>
  1865. <Tag size='small' color='grey'>
  1866. {(operation.conditions || []).length}
  1867. </Tag>
  1868. </div>
  1869. <Space spacing={6} style={{ marginTop: 8 }}>
  1870. <Tag
  1871. size='small'
  1872. color={getOperationModeTagColor(
  1873. operation.mode || 'set',
  1874. )}
  1875. >
  1876. {OPERATION_MODE_LABEL_MAP[
  1877. operation.mode || 'set'
  1878. ] ||
  1879. operation.mode ||
  1880. 'set'}
  1881. </Tag>
  1882. <Text type='tertiary' size='small'>
  1883. {t('条件数')}
  1884. </Text>
  1885. </Space>
  1886. </div>
  1887. );
  1888. })}
  1889. </div>
  1890. )}
  1891. </div>
  1892. </Card>
  1893. </Col>
  1894. <Col xs={24} md={16}>
  1895. {selectedOperation ? (
  1896. (() => {
  1897. const mode = selectedOperation.mode || 'set';
  1898. const meta = MODE_META[mode] || MODE_META.set;
  1899. const conditions = selectedOperation.conditions || [];
  1900. const syncFromTarget =
  1901. mode === 'sync_fields'
  1902. ? parseSyncTargetSpec(selectedOperation.from)
  1903. : null;
  1904. const syncToTarget =
  1905. mode === 'sync_fields'
  1906. ? parseSyncTargetSpec(selectedOperation.to)
  1907. : null;
  1908. return (
  1909. <Card
  1910. className='!rounded-2xl !border-0'
  1911. bodyStyle={{
  1912. padding: 14,
  1913. background: 'var(--semi-color-fill-0)',
  1914. }}
  1915. >
  1916. <div className='flex items-center justify-between mb-3'>
  1917. <Space>
  1918. <Tag color='blue'>{`#${selectedOperationIndex + 1}`}</Tag>
  1919. <Text strong>
  1920. {getOperationSummary(
  1921. selectedOperation,
  1922. selectedOperationIndex,
  1923. )}
  1924. </Text>
  1925. </Space>
  1926. <Space>
  1927. <Button
  1928. size='small'
  1929. type='tertiary'
  1930. onClick={() =>
  1931. duplicateOperation(selectedOperation.id)
  1932. }
  1933. >
  1934. {t('复制')}
  1935. </Button>
  1936. <Button
  1937. size='small'
  1938. type='danger'
  1939. theme='borderless'
  1940. icon={<IconDelete />}
  1941. aria-label={t('删除规则')}
  1942. onClick={() =>
  1943. removeOperation(selectedOperation.id)
  1944. }
  1945. />
  1946. </Space>
  1947. </div>
  1948. <Row gutter={12}>
  1949. <Col xs={24} md={8}>
  1950. <Text type='tertiary' size='small'>
  1951. {t('操作类型')}
  1952. </Text>
  1953. <Select
  1954. value={mode}
  1955. optionList={OPERATION_MODE_OPTIONS}
  1956. onChange={(nextMode) =>
  1957. updateOperation(selectedOperation.id, {
  1958. mode: nextMode,
  1959. })
  1960. }
  1961. style={{ width: '100%' }}
  1962. />
  1963. </Col>
  1964. {meta.path || meta.pathOptional ? (
  1965. <Col xs={24} md={16}>
  1966. <Text type='tertiary' size='small'>
  1967. {meta.pathOptional
  1968. ? t('目标路径(可选)')
  1969. : t(getModePathLabel(mode))}
  1970. </Text>
  1971. <Input
  1972. value={selectedOperation.path}
  1973. placeholder={getModePathPlaceholder(mode)}
  1974. onChange={(nextValue) =>
  1975. updateOperation(selectedOperation.id, {
  1976. path: nextValue,
  1977. })
  1978. }
  1979. />
  1980. </Col>
  1981. ) : null}
  1982. </Row>
  1983. <Text
  1984. type='tertiary'
  1985. size='small'
  1986. className='mt-1 block'
  1987. >
  1988. {MODE_DESCRIPTIONS[mode] || ''}
  1989. </Text>
  1990. <div className='mt-2'>
  1991. <Text type='tertiary' size='small'>
  1992. {t('规则描述(可选)')}
  1993. </Text>
  1994. <Input
  1995. value={selectedOperation.description || ''}
  1996. placeholder={t('例如:清理工具参数,避免上游校验错误')}
  1997. onChange={(nextValue) =>
  1998. updateOperation(selectedOperation.id, {
  1999. description: nextValue || '',
  2000. })
  2001. }
  2002. maxLength={180}
  2003. showClear
  2004. />
  2005. <Text type='tertiary' size='small' className='mt-1 block'>
  2006. {`${String(selectedOperation.description || '').length}/180`}
  2007. </Text>
  2008. </div>
  2009. {meta.value ? (
  2010. mode === 'return_error' && returnErrorDraft ? (
  2011. <div
  2012. className='mt-2 rounded-xl p-3'
  2013. style={{
  2014. background: 'var(--semi-color-bg-1)',
  2015. border: '1px solid var(--semi-color-border)',
  2016. }}
  2017. >
  2018. <div className='flex items-center justify-between mb-2'>
  2019. <Text strong>{t('自定义错误响应')}</Text>
  2020. <Space spacing={6} align='center'>
  2021. <Text type='tertiary' size='small'>
  2022. {t('模式')}
  2023. </Text>
  2024. <Button
  2025. size='small'
  2026. type={
  2027. returnErrorDraft.simpleMode
  2028. ? 'primary'
  2029. : 'tertiary'
  2030. }
  2031. onClick={() =>
  2032. updateReturnErrorDraft(
  2033. selectedOperation.id,
  2034. { simpleMode: true },
  2035. )
  2036. }
  2037. >
  2038. {t('简洁')}
  2039. </Button>
  2040. <Button
  2041. size='small'
  2042. type={
  2043. returnErrorDraft.simpleMode
  2044. ? 'tertiary'
  2045. : 'primary'
  2046. }
  2047. onClick={() =>
  2048. updateReturnErrorDraft(
  2049. selectedOperation.id,
  2050. { simpleMode: false },
  2051. )
  2052. }
  2053. >
  2054. {t('高级')}
  2055. </Button>
  2056. </Space>
  2057. </div>
  2058. <Text type='tertiary' size='small'>
  2059. {t('错误消息(必填)')}
  2060. </Text>
  2061. <TextArea
  2062. value={returnErrorDraft.message}
  2063. autosize={{ minRows: 2, maxRows: 4 }}
  2064. placeholder={t('例如:该请求不满足准入策略')}
  2065. onChange={(nextValue) =>
  2066. updateReturnErrorDraft(
  2067. selectedOperation.id,
  2068. { message: nextValue },
  2069. )
  2070. }
  2071. />
  2072. {returnErrorDraft.simpleMode ? (
  2073. <Text
  2074. type='tertiary'
  2075. size='small'
  2076. className='mt-2 block'
  2077. >
  2078. {t(
  2079. '简洁模式仅返回 message;状态码和错误类型将使用系统默认值。',
  2080. )}
  2081. </Text>
  2082. ) : (
  2083. <>
  2084. <Row gutter={12} style={{ marginTop: 10 }}>
  2085. <Col xs={24} md={8}>
  2086. <Text type='tertiary' size='small'>
  2087. {t('状态码')}
  2088. </Text>
  2089. <Input
  2090. value={String(
  2091. returnErrorDraft.statusCode ?? '',
  2092. )}
  2093. placeholder='400'
  2094. onChange={(nextValue) =>
  2095. updateReturnErrorDraft(
  2096. selectedOperation.id,
  2097. {
  2098. statusCode:
  2099. parseInt(nextValue, 10) ||
  2100. 400,
  2101. },
  2102. )
  2103. }
  2104. />
  2105. </Col>
  2106. <Col xs={24} md={8}>
  2107. <Text type='tertiary' size='small'>
  2108. {t('错误代码(可选)')}
  2109. </Text>
  2110. <Input
  2111. value={returnErrorDraft.code}
  2112. placeholder='forced_bad_request'
  2113. onChange={(nextValue) =>
  2114. updateReturnErrorDraft(
  2115. selectedOperation.id,
  2116. { code: nextValue },
  2117. )
  2118. }
  2119. />
  2120. </Col>
  2121. <Col xs={24} md={8}>
  2122. <Text type='tertiary' size='small'>
  2123. {t('错误类型(可选)')}
  2124. </Text>
  2125. <Input
  2126. value={returnErrorDraft.type}
  2127. placeholder='invalid_request_error'
  2128. onChange={(nextValue) =>
  2129. updateReturnErrorDraft(
  2130. selectedOperation.id,
  2131. { type: nextValue },
  2132. )
  2133. }
  2134. />
  2135. </Col>
  2136. </Row>
  2137. <div className='mt-2 flex items-center gap-2'>
  2138. <Text type='tertiary' size='small'>
  2139. {t('重试建议')}
  2140. </Text>
  2141. <Button
  2142. size='small'
  2143. type={
  2144. returnErrorDraft.skipRetry
  2145. ? 'primary'
  2146. : 'tertiary'
  2147. }
  2148. onClick={() =>
  2149. updateReturnErrorDraft(
  2150. selectedOperation.id,
  2151. { skipRetry: true },
  2152. )
  2153. }
  2154. >
  2155. {t('停止重试')}
  2156. </Button>
  2157. <Button
  2158. size='small'
  2159. type={
  2160. returnErrorDraft.skipRetry
  2161. ? 'tertiary'
  2162. : 'primary'
  2163. }
  2164. onClick={() =>
  2165. updateReturnErrorDraft(
  2166. selectedOperation.id,
  2167. { skipRetry: false },
  2168. )
  2169. }
  2170. >
  2171. {t('允许重试')}
  2172. </Button>
  2173. </div>
  2174. <Space wrap style={{ marginTop: 8 }}>
  2175. <Tag
  2176. size='small'
  2177. color='grey'
  2178. className='cursor-pointer'
  2179. onClick={() =>
  2180. updateReturnErrorDraft(
  2181. selectedOperation.id,
  2182. {
  2183. statusCode: 400,
  2184. code: 'invalid_request',
  2185. type: 'invalid_request_error',
  2186. },
  2187. )
  2188. }
  2189. >
  2190. {t('参数错误')}
  2191. </Tag>
  2192. <Tag
  2193. size='small'
  2194. color='grey'
  2195. className='cursor-pointer'
  2196. onClick={() =>
  2197. updateReturnErrorDraft(
  2198. selectedOperation.id,
  2199. {
  2200. statusCode: 401,
  2201. code: 'unauthorized',
  2202. type: 'authentication_error',
  2203. },
  2204. )
  2205. }
  2206. >
  2207. {t('未授权')}
  2208. </Tag>
  2209. <Tag
  2210. size='small'
  2211. color='grey'
  2212. className='cursor-pointer'
  2213. onClick={() =>
  2214. updateReturnErrorDraft(
  2215. selectedOperation.id,
  2216. {
  2217. statusCode: 429,
  2218. code: 'rate_limited',
  2219. type: 'rate_limit_error',
  2220. },
  2221. )
  2222. }
  2223. >
  2224. {t('限流')}
  2225. </Tag>
  2226. </Space>
  2227. </>
  2228. )}
  2229. </div>
  2230. ) : mode === 'prune_objects' && pruneObjectsDraft ? (
  2231. <div
  2232. className='mt-2 rounded-xl p-3'
  2233. style={{
  2234. background: 'var(--semi-color-bg-1)',
  2235. border: '1px solid var(--semi-color-border)',
  2236. }}
  2237. >
  2238. <div className='flex items-center justify-between mb-2'>
  2239. <Text strong>{t('对象清理规则')}</Text>
  2240. <Space spacing={6} align='center'>
  2241. <Text type='tertiary' size='small'>
  2242. {t('模式')}
  2243. </Text>
  2244. <Button
  2245. size='small'
  2246. type={
  2247. pruneObjectsDraft.simpleMode
  2248. ? 'primary'
  2249. : 'tertiary'
  2250. }
  2251. onClick={() =>
  2252. updatePruneObjectsDraft(
  2253. selectedOperation.id,
  2254. { simpleMode: true },
  2255. )
  2256. }
  2257. >
  2258. {t('简洁')}
  2259. </Button>
  2260. <Button
  2261. size='small'
  2262. type={
  2263. pruneObjectsDraft.simpleMode
  2264. ? 'tertiary'
  2265. : 'primary'
  2266. }
  2267. onClick={() =>
  2268. updatePruneObjectsDraft(
  2269. selectedOperation.id,
  2270. { simpleMode: false },
  2271. )
  2272. }
  2273. >
  2274. {t('高级')}
  2275. </Button>
  2276. </Space>
  2277. </div>
  2278. <Text type='tertiary' size='small'>
  2279. {t('类型(常用)')}
  2280. </Text>
  2281. <Input
  2282. value={pruneObjectsDraft.typeText}
  2283. placeholder='redacted_thinking'
  2284. onChange={(nextValue) =>
  2285. updatePruneObjectsDraft(
  2286. selectedOperation.id,
  2287. { typeText: nextValue },
  2288. )
  2289. }
  2290. />
  2291. {pruneObjectsDraft.simpleMode ? (
  2292. <Text
  2293. type='tertiary'
  2294. size='small'
  2295. className='mt-2 block'
  2296. >
  2297. {t(
  2298. '简洁模式:按 type 全量清理对象,例如 redacted_thinking。',
  2299. )}
  2300. </Text>
  2301. ) : (
  2302. <>
  2303. <Row gutter={12} style={{ marginTop: 10 }}>
  2304. <Col xs={24} md={12}>
  2305. <Text type='tertiary' size='small'>
  2306. {t('逻辑')}
  2307. </Text>
  2308. <Select
  2309. value={pruneObjectsDraft.logic}
  2310. optionList={[
  2311. { label: t('全部满足(AND)'), value: 'AND' },
  2312. { label: t('任一满足(OR)'), value: 'OR' },
  2313. ]}
  2314. style={{ width: '100%' }}
  2315. onChange={(nextValue) =>
  2316. updatePruneObjectsDraft(
  2317. selectedOperation.id,
  2318. { logic: nextValue || 'AND' },
  2319. )
  2320. }
  2321. />
  2322. </Col>
  2323. <Col xs={24} md={12}>
  2324. <Text type='tertiary' size='small'>
  2325. {t('递归策略')}
  2326. </Text>
  2327. <Space spacing={6} style={{ marginTop: 2 }}>
  2328. <Button
  2329. size='small'
  2330. type={
  2331. pruneObjectsDraft.recursive
  2332. ? 'primary'
  2333. : 'tertiary'
  2334. }
  2335. onClick={() =>
  2336. updatePruneObjectsDraft(
  2337. selectedOperation.id,
  2338. { recursive: true },
  2339. )
  2340. }
  2341. >
  2342. {t('递归')}
  2343. </Button>
  2344. <Button
  2345. size='small'
  2346. type={
  2347. pruneObjectsDraft.recursive
  2348. ? 'tertiary'
  2349. : 'primary'
  2350. }
  2351. onClick={() =>
  2352. updatePruneObjectsDraft(
  2353. selectedOperation.id,
  2354. { recursive: false },
  2355. )
  2356. }
  2357. >
  2358. {t('仅当前层')}
  2359. </Button>
  2360. </Space>
  2361. </Col>
  2362. </Row>
  2363. <div
  2364. className='mt-2 rounded-lg p-2'
  2365. style={{
  2366. background: 'var(--semi-color-fill-0)',
  2367. }}
  2368. >
  2369. <div className='flex items-center justify-between mb-2'>
  2370. <Text strong>
  2371. {t('附加条件')}
  2372. </Text>
  2373. <Button
  2374. size='small'
  2375. icon={<IconPlus />}
  2376. onClick={() =>
  2377. addPruneRule(selectedOperation.id)
  2378. }
  2379. >
  2380. {t('新增条件')}
  2381. </Button>
  2382. </div>
  2383. {(pruneObjectsDraft.rules || []).length === 0 ? (
  2384. <Text type='tertiary' size='small'>
  2385. {t(
  2386. '未添加附加条件时,仅使用上方 type 进行清理。',
  2387. )}
  2388. </Text>
  2389. ) : (
  2390. <div className='flex flex-col gap-2'>
  2391. {(pruneObjectsDraft.rules || []).map(
  2392. (rule, ruleIndex) => (
  2393. <div
  2394. key={rule.id}
  2395. className='rounded-lg p-2'
  2396. style={{
  2397. border:
  2398. '1px solid var(--semi-color-border)',
  2399. background:
  2400. 'var(--semi-color-bg-0)',
  2401. }}
  2402. >
  2403. <div className='flex items-center justify-between mb-2'>
  2404. <Tag size='small'>
  2405. {`R${ruleIndex + 1}`}
  2406. </Tag>
  2407. <Button
  2408. size='small'
  2409. type='danger'
  2410. theme='borderless'
  2411. icon={<IconDelete />}
  2412. onClick={() =>
  2413. removePruneRule(
  2414. selectedOperation.id,
  2415. rule.id,
  2416. )
  2417. }
  2418. >
  2419. {t('删除条件')}
  2420. </Button>
  2421. </div>
  2422. <Row gutter={8}>
  2423. <Col xs={24} md={9}>
  2424. <Text
  2425. type='tertiary'
  2426. size='small'
  2427. >
  2428. {t('字段路径')}
  2429. </Text>
  2430. <Input
  2431. value={rule.path}
  2432. placeholder='type'
  2433. onChange={(nextValue) =>
  2434. updatePruneRule(
  2435. selectedOperation.id,
  2436. rule.id,
  2437. { path: nextValue },
  2438. )
  2439. }
  2440. />
  2441. </Col>
  2442. <Col xs={24} md={7}>
  2443. <Text
  2444. type='tertiary'
  2445. size='small'
  2446. >
  2447. {t('匹配方式')}
  2448. </Text>
  2449. <Select
  2450. value={rule.mode}
  2451. optionList={
  2452. CONDITION_MODE_OPTIONS
  2453. }
  2454. style={{ width: '100%' }}
  2455. onChange={(nextValue) =>
  2456. updatePruneRule(
  2457. selectedOperation.id,
  2458. rule.id,
  2459. { mode: nextValue },
  2460. )
  2461. }
  2462. />
  2463. </Col>
  2464. <Col xs={24} md={8}>
  2465. <Text
  2466. type='tertiary'
  2467. size='small'
  2468. >
  2469. {t('匹配值(可选)')}
  2470. </Text>
  2471. <Input
  2472. value={rule.value_text}
  2473. placeholder='redacted_thinking'
  2474. onChange={(nextValue) =>
  2475. updatePruneRule(
  2476. selectedOperation.id,
  2477. rule.id,
  2478. {
  2479. value_text:
  2480. nextValue,
  2481. },
  2482. )
  2483. }
  2484. />
  2485. </Col>
  2486. </Row>
  2487. <Space
  2488. wrap
  2489. spacing={8}
  2490. style={{ marginTop: 8 }}
  2491. >
  2492. <Button
  2493. size='small'
  2494. type={
  2495. rule.invert
  2496. ? 'primary'
  2497. : 'tertiary'
  2498. }
  2499. onClick={() =>
  2500. updatePruneRule(
  2501. selectedOperation.id,
  2502. rule.id,
  2503. {
  2504. invert:
  2505. !rule.invert,
  2506. },
  2507. )
  2508. }
  2509. >
  2510. {t('条件取反')}
  2511. </Button>
  2512. <Button
  2513. size='small'
  2514. type={
  2515. rule.pass_missing_key
  2516. ? 'primary'
  2517. : 'tertiary'
  2518. }
  2519. onClick={() =>
  2520. updatePruneRule(
  2521. selectedOperation.id,
  2522. rule.id,
  2523. {
  2524. pass_missing_key:
  2525. !rule.pass_missing_key,
  2526. },
  2527. )
  2528. }
  2529. >
  2530. {t('字段缺失视为命中')}
  2531. </Button>
  2532. </Space>
  2533. </div>
  2534. ),
  2535. )}
  2536. </div>
  2537. )}
  2538. </div>
  2539. </>
  2540. )}
  2541. </div>
  2542. ) : (
  2543. <div className='mt-2'>
  2544. <div className='flex items-center justify-between gap-2'>
  2545. <Text type='tertiary' size='small'>
  2546. {t(getModeValueLabel(mode))}
  2547. </Text>
  2548. {mode === 'set_header' ? (
  2549. <Button
  2550. size='small'
  2551. type='tertiary'
  2552. onClick={formatSelectedOperationValueAsJson}
  2553. >
  2554. {t('格式化 JSON')}
  2555. </Button>
  2556. ) : null}
  2557. </div>
  2558. <TextArea
  2559. value={selectedOperation.value_text}
  2560. autosize={{ minRows: 1, maxRows: 4 }}
  2561. placeholder={getModeValuePlaceholder(mode)}
  2562. onChange={(nextValue) =>
  2563. updateOperation(selectedOperation.id, {
  2564. value_text: nextValue,
  2565. })
  2566. }
  2567. />
  2568. {getModeValueHelp(mode) ? (
  2569. <Text type='tertiary' size='small'>
  2570. {t(getModeValueHelp(mode))}
  2571. </Text>
  2572. ) : null}
  2573. </div>
  2574. )
  2575. ) : null}
  2576. {meta.keepOrigin ? (
  2577. <div className='mt-2 flex items-center gap-2'>
  2578. <Switch
  2579. checked={Boolean(
  2580. selectedOperation.keep_origin,
  2581. )}
  2582. checkedText={t('开')}
  2583. uncheckedText={t('关')}
  2584. onChange={(nextValue) =>
  2585. updateOperation(selectedOperation.id, {
  2586. keep_origin: nextValue,
  2587. })
  2588. }
  2589. />
  2590. <Text
  2591. type='tertiary'
  2592. size='small'
  2593. className='leading-6'
  2594. >
  2595. {t('保留原值(目标已有值时不覆盖)')}
  2596. </Text>
  2597. </div>
  2598. ) : null}
  2599. {mode === 'sync_fields' ? (
  2600. <div className='mt-2'>
  2601. <Text type='tertiary' size='small'>
  2602. {t('同步端点')}
  2603. </Text>
  2604. <Row gutter={12} style={{ marginTop: 6 }}>
  2605. <Col xs={24} md={12}>
  2606. <Text type='tertiary' size='small'>
  2607. {t('来源端点')}
  2608. </Text>
  2609. <div className='flex gap-2'>
  2610. <Select
  2611. value={syncFromTarget?.type || 'json'}
  2612. optionList={SYNC_TARGET_TYPE_OPTIONS}
  2613. style={{ width: 120 }}
  2614. onChange={(nextType) =>
  2615. updateOperation(
  2616. selectedOperation.id,
  2617. {
  2618. from: buildSyncTargetSpec(
  2619. nextType,
  2620. syncFromTarget?.key || '',
  2621. ),
  2622. },
  2623. )
  2624. }
  2625. />
  2626. <Input
  2627. value={syncFromTarget?.key || ''}
  2628. placeholder='session_id'
  2629. onChange={(nextKey) =>
  2630. updateOperation(
  2631. selectedOperation.id,
  2632. {
  2633. from: buildSyncTargetSpec(
  2634. syncFromTarget?.type || 'json',
  2635. nextKey,
  2636. ),
  2637. },
  2638. )
  2639. }
  2640. />
  2641. </div>
  2642. </Col>
  2643. <Col xs={24} md={12}>
  2644. <Text type='tertiary' size='small'>
  2645. {t('目标端点')}
  2646. </Text>
  2647. <div className='flex gap-2'>
  2648. <Select
  2649. value={syncToTarget?.type || 'json'}
  2650. optionList={SYNC_TARGET_TYPE_OPTIONS}
  2651. style={{ width: 120 }}
  2652. onChange={(nextType) =>
  2653. updateOperation(
  2654. selectedOperation.id,
  2655. {
  2656. to: buildSyncTargetSpec(
  2657. nextType,
  2658. syncToTarget?.key || '',
  2659. ),
  2660. },
  2661. )
  2662. }
  2663. />
  2664. <Input
  2665. value={syncToTarget?.key || ''}
  2666. placeholder='prompt_cache_key'
  2667. onChange={(nextKey) =>
  2668. updateOperation(
  2669. selectedOperation.id,
  2670. {
  2671. to: buildSyncTargetSpec(
  2672. syncToTarget?.type || 'json',
  2673. nextKey,
  2674. ),
  2675. },
  2676. )
  2677. }
  2678. />
  2679. </div>
  2680. </Col>
  2681. </Row>
  2682. <Space wrap style={{ marginTop: 8 }}>
  2683. <Tag
  2684. size='small'
  2685. color='cyan'
  2686. className='cursor-pointer'
  2687. onClick={() =>
  2688. updateOperation(selectedOperation.id, {
  2689. from: 'header:session_id',
  2690. to: 'json:prompt_cache_key',
  2691. })
  2692. }
  2693. >
  2694. {
  2695. 'header:session_id -> json:prompt_cache_key'
  2696. }
  2697. </Tag>
  2698. <Tag
  2699. size='small'
  2700. color='cyan'
  2701. className='cursor-pointer'
  2702. onClick={() =>
  2703. updateOperation(selectedOperation.id, {
  2704. from: 'json:prompt_cache_key',
  2705. to: 'header:session_id',
  2706. })
  2707. }
  2708. >
  2709. {
  2710. 'json:prompt_cache_key -> header:session_id'
  2711. }
  2712. </Tag>
  2713. </Space>
  2714. </div>
  2715. ) : meta.from || meta.to === false || meta.to ? (
  2716. <Row gutter={12} style={{ marginTop: 8 }}>
  2717. {meta.from || meta.to === false ? (
  2718. <Col xs={24} md={12}>
  2719. <Text type='tertiary' size='small'>
  2720. {t(getModeFromLabel(mode))}
  2721. </Text>
  2722. <Input
  2723. value={selectedOperation.from}
  2724. placeholder={getModeFromPlaceholder(mode)}
  2725. onChange={(nextValue) =>
  2726. updateOperation(selectedOperation.id, {
  2727. from: nextValue,
  2728. })
  2729. }
  2730. />
  2731. </Col>
  2732. ) : null}
  2733. {meta.to || meta.to === false ? (
  2734. <Col xs={24} md={12}>
  2735. <Text type='tertiary' size='small'>
  2736. {t(getModeToLabel(mode))}
  2737. </Text>
  2738. <Input
  2739. value={selectedOperation.to}
  2740. placeholder={getModeToPlaceholder(mode)}
  2741. onChange={(nextValue) =>
  2742. updateOperation(selectedOperation.id, {
  2743. to: nextValue,
  2744. })
  2745. }
  2746. />
  2747. </Col>
  2748. ) : null}
  2749. </Row>
  2750. ) : null}
  2751. <div
  2752. className='mt-3 rounded-xl p-3'
  2753. style={{
  2754. background: 'rgba(127, 127, 127, 0.08)',
  2755. }}
  2756. >
  2757. <div className='flex items-center justify-between mb-2'>
  2758. <Space align='center'>
  2759. <Text>{t('条件规则')}</Text>
  2760. <Select
  2761. value={selectedOperation.logic || 'OR'}
  2762. optionList={[
  2763. { label: t('满足任一条件(OR)'), value: 'OR' },
  2764. { label: t('必须全部满足(AND)'), value: 'AND' },
  2765. ]}
  2766. size='small'
  2767. style={{ width: 180 }}
  2768. onChange={(nextValue) =>
  2769. updateOperation(selectedOperation.id, {
  2770. logic: nextValue,
  2771. })
  2772. }
  2773. />
  2774. </Space>
  2775. <Space spacing={6}>
  2776. <Button
  2777. size='small'
  2778. type='tertiary'
  2779. onClick={expandAllSelectedConditions}
  2780. >
  2781. {t('全部展开')}
  2782. </Button>
  2783. <Button
  2784. size='small'
  2785. type='tertiary'
  2786. onClick={collapseAllSelectedConditions}
  2787. >
  2788. {t('全部收起')}
  2789. </Button>
  2790. <Button
  2791. icon={<IconPlus />}
  2792. size='small'
  2793. onClick={() =>
  2794. addCondition(selectedOperation.id)
  2795. }
  2796. >
  2797. {t('新增条件')}
  2798. </Button>
  2799. </Space>
  2800. </div>
  2801. {conditions.length === 0 ? (
  2802. <Text type='tertiary' size='small'>
  2803. {t('没有条件时,默认总是执行该操作。')}
  2804. </Text>
  2805. ) : (
  2806. <Collapse
  2807. keepDOM
  2808. activeKey={selectedConditionKeys}
  2809. onChange={(activeKeys) =>
  2810. handleConditionCollapseChange(
  2811. selectedOperation.id,
  2812. activeKeys,
  2813. )
  2814. }
  2815. >
  2816. {conditions.map(
  2817. (condition, conditionIndex) => (
  2818. <Collapse.Panel
  2819. key={condition.id}
  2820. itemKey={condition.id}
  2821. header={
  2822. <Space spacing={8}>
  2823. <Tag size='small'>
  2824. {`C${conditionIndex + 1}`}
  2825. </Tag>
  2826. <Text type='tertiary' size='small'>
  2827. {condition.path ||
  2828. t('未设置路径')}
  2829. </Text>
  2830. </Space>
  2831. }
  2832. >
  2833. <div>
  2834. <div className='flex items-center justify-between mb-2'>
  2835. <Text type='tertiary' size='small'>
  2836. {t('条件项设置')}
  2837. </Text>
  2838. <Button
  2839. theme='borderless'
  2840. type='danger'
  2841. icon={<IconDelete />}
  2842. size='small'
  2843. onClick={() =>
  2844. removeCondition(
  2845. selectedOperation.id,
  2846. condition.id,
  2847. )
  2848. }
  2849. >
  2850. {t('删除条件')}
  2851. </Button>
  2852. </div>
  2853. <Row gutter={12}>
  2854. <Col xs={24} md={10}>
  2855. <Text
  2856. type='tertiary'
  2857. size='small'
  2858. >
  2859. {t('字段路径')}
  2860. </Text>
  2861. <Input
  2862. value={condition.path}
  2863. placeholder='model'
  2864. onChange={(nextValue) =>
  2865. updateCondition(
  2866. selectedOperation.id,
  2867. condition.id,
  2868. { path: nextValue },
  2869. )
  2870. }
  2871. />
  2872. </Col>
  2873. <Col xs={24} md={8}>
  2874. <Text
  2875. type='tertiary'
  2876. size='small'
  2877. >
  2878. {t('匹配方式')}
  2879. </Text>
  2880. <Select
  2881. value={condition.mode}
  2882. optionList={
  2883. CONDITION_MODE_OPTIONS
  2884. }
  2885. onChange={(nextValue) =>
  2886. updateCondition(
  2887. selectedOperation.id,
  2888. condition.id,
  2889. { mode: nextValue },
  2890. )
  2891. }
  2892. style={{ width: '100%' }}
  2893. />
  2894. </Col>
  2895. <Col xs={24} md={6}>
  2896. <Text
  2897. type='tertiary'
  2898. size='small'
  2899. >
  2900. {t('匹配值')}
  2901. </Text>
  2902. <Input
  2903. value={condition.value_text}
  2904. placeholder='gpt'
  2905. onChange={(nextValue) =>
  2906. updateCondition(
  2907. selectedOperation.id,
  2908. condition.id,
  2909. { value_text: nextValue },
  2910. )
  2911. }
  2912. />
  2913. </Col>
  2914. </Row>
  2915. <div className='mt-2 flex flex-wrap gap-3'>
  2916. <div className='flex items-center gap-2'>
  2917. <Text type='tertiary' size='small'>
  2918. {t('条件取反')}
  2919. </Text>
  2920. <Switch
  2921. checked={Boolean(
  2922. condition.invert,
  2923. )}
  2924. checkedText={t('开')}
  2925. uncheckedText={t('关')}
  2926. onChange={(nextValue) =>
  2927. updateCondition(
  2928. selectedOperation.id,
  2929. condition.id,
  2930. { invert: nextValue },
  2931. )
  2932. }
  2933. />
  2934. </div>
  2935. <div className='flex items-center gap-2'>
  2936. <Text type='tertiary' size='small'>
  2937. {t('字段缺失视为命中')}
  2938. </Text>
  2939. <Switch
  2940. checked={Boolean(
  2941. condition.pass_missing_key,
  2942. )}
  2943. checkedText={t('开')}
  2944. uncheckedText={t('关')}
  2945. onChange={(nextValue) =>
  2946. updateCondition(
  2947. selectedOperation.id,
  2948. condition.id,
  2949. {
  2950. pass_missing_key: nextValue,
  2951. },
  2952. )
  2953. }
  2954. />
  2955. </div>
  2956. </div>
  2957. </div>
  2958. </Collapse.Panel>
  2959. ),
  2960. )}
  2961. </Collapse>
  2962. )}
  2963. </div>
  2964. </Card>
  2965. );
  2966. })()
  2967. ) : (
  2968. <Card
  2969. className='!rounded-2xl !border-0'
  2970. bodyStyle={{
  2971. padding: 14,
  2972. background: 'var(--semi-color-fill-0)',
  2973. }}
  2974. >
  2975. <Text type='tertiary'>
  2976. {t('请选择一条规则进行编辑。')}
  2977. </Text>
  2978. </Card>
  2979. )}
  2980. {visualValidationError ? (
  2981. <Card
  2982. className='!rounded-2xl !border-0 mt-3'
  2983. bodyStyle={{
  2984. padding: 12,
  2985. background: 'var(--semi-color-fill-0)',
  2986. }}
  2987. >
  2988. <Space>
  2989. <Tag color='red'>{t('暂存错误')}</Tag>
  2990. <Text type='danger'>{visualValidationError}</Text>
  2991. </Space>
  2992. </Card>
  2993. ) : null}
  2994. </Col>
  2995. </Row>
  2996. </div>
  2997. )}
  2998. </div>
  2999. ) : (
  3000. <div style={{ width: '100%' }}>
  3001. <Space style={{ marginBottom: 8 }} wrap>
  3002. <Button onClick={formatJson}>{t('格式化')}</Button>
  3003. <Tag color='grey'>{t('高级文本编辑')}</Tag>
  3004. </Space>
  3005. <TextArea
  3006. value={jsonText}
  3007. autosize={{ minRows: 18, maxRows: 28 }}
  3008. onChange={(nextValue) => handleJsonChange(nextValue ?? '')}
  3009. placeholder={JSON.stringify(OPERATION_TEMPLATE, null, 2)}
  3010. showClear
  3011. />
  3012. <Text type='tertiary' size='small' className='mt-2 block'>
  3013. {t('直接编辑 JSON 文本,保存时会校验格式。')}
  3014. </Text>
  3015. {jsonError ? (
  3016. <Text className='text-red-500 text-xs mt-2'>{jsonError}</Text>
  3017. ) : null}
  3018. </div>
  3019. )}
  3020. </Space>
  3021. </Modal>
  3022. <Modal
  3023. title={null}
  3024. visible={fieldGuideVisible}
  3025. width={860}
  3026. footer={null}
  3027. onCancel={() => setFieldGuideVisible(false)}
  3028. bodyStyle={{
  3029. maxHeight: '72vh',
  3030. overflowY: 'auto',
  3031. padding: 16,
  3032. background: 'var(--semi-color-bg-0)',
  3033. }}
  3034. >
  3035. <Space vertical spacing={12} style={{ width: '100%' }}>
  3036. <div className='flex items-start justify-between gap-3'>
  3037. <div>
  3038. <Text strong style={{ fontSize: 22, lineHeight: '30px' }}>
  3039. {t('字段速查')}
  3040. </Text>
  3041. <Text
  3042. type='tertiary'
  3043. size='small'
  3044. className='block mt-1'
  3045. style={{ maxWidth: 560 }}
  3046. >
  3047. {t(
  3048. '先搜索,再一键复制字段名或填入当前规则。字段名为系统内部路径,可直接用于路径 / 来源 / 目标。',
  3049. )}
  3050. </Text>
  3051. </div>
  3052. <Tag color='blue'>{`${fieldGuideFieldCount} ${t('个字段')}`}</Tag>
  3053. </div>
  3054. <Card
  3055. className='!rounded-xl !border-0'
  3056. bodyStyle={{
  3057. padding: 12,
  3058. background: 'var(--semi-color-fill-0)',
  3059. }}
  3060. >
  3061. <div className='flex items-center gap-2'>
  3062. <Input
  3063. value={fieldGuideKeyword}
  3064. onChange={(nextValue) => setFieldGuideKeyword(nextValue || '')}
  3065. placeholder={t('搜索字段名 / 中文说明')}
  3066. showClear
  3067. style={{ flex: 1 }}
  3068. />
  3069. <Select
  3070. value={fieldGuideTarget}
  3071. optionList={FIELD_GUIDE_TARGET_OPTIONS}
  3072. onChange={(nextValue) =>
  3073. setFieldGuideTarget(nextValue || 'path')
  3074. }
  3075. style={{ width: 170 }}
  3076. />
  3077. </div>
  3078. </Card>
  3079. {filteredFieldGuideSections.length === 0 ? (
  3080. <Card
  3081. className='!rounded-xl !border-0'
  3082. bodyStyle={{
  3083. padding: 20,
  3084. background: 'var(--semi-color-fill-0)',
  3085. }}
  3086. >
  3087. <Text type='tertiary'>{t('没有匹配的字段')}</Text>
  3088. </Card>
  3089. ) : (
  3090. <div className='flex flex-col gap-2'>
  3091. {filteredFieldGuideSections.map((section) => (
  3092. <Card
  3093. key={section.title}
  3094. className='!rounded-xl !border-0'
  3095. bodyStyle={{
  3096. padding: 14,
  3097. background: 'var(--semi-color-fill-0)',
  3098. }}
  3099. >
  3100. <div className='flex items-center justify-between mb-1'>
  3101. <Text strong style={{ fontSize: 18 }}>
  3102. {section.title}
  3103. </Text>
  3104. <Tag color='grey'>{`${section.fields.length} ${t('项')}`}</Tag>
  3105. </div>
  3106. <div
  3107. style={{
  3108. display: 'flex',
  3109. flexDirection: 'column',
  3110. marginTop: 6,
  3111. }}
  3112. >
  3113. {section.fields.map((field, index) => (
  3114. <div
  3115. key={field.key}
  3116. className='flex items-start justify-between gap-3'
  3117. style={{
  3118. paddingTop: 10,
  3119. paddingBottom: 10,
  3120. borderTop:
  3121. index === 0
  3122. ? 'none'
  3123. : '1px solid var(--semi-color-border)',
  3124. }}
  3125. >
  3126. <div style={{ flex: 1, minWidth: 0 }}>
  3127. <Text strong>{field.label}</Text>
  3128. <Text
  3129. type='secondary'
  3130. size='small'
  3131. className='block mt-1 font-mono'
  3132. style={{
  3133. background: 'var(--semi-color-bg-1)',
  3134. border: '1px solid var(--semi-color-border)',
  3135. borderRadius: 8,
  3136. padding: '4px 8px',
  3137. width: 'fit-content',
  3138. }}
  3139. >
  3140. {field.key}
  3141. </Text>
  3142. <Text
  3143. type='tertiary'
  3144. size='small'
  3145. className='block mt-1'
  3146. style={{ lineHeight: '18px' }}
  3147. >
  3148. {field.tip}
  3149. </Text>
  3150. </div>
  3151. <Space spacing={6} align='center'>
  3152. <Button
  3153. size='small'
  3154. type='tertiary'
  3155. onClick={() => copyBuiltinField(field.key)}
  3156. >
  3157. {t('复制')}
  3158. </Button>
  3159. <Button
  3160. size='small'
  3161. onClick={() =>
  3162. applyBuiltinField(field.key, fieldGuideTarget)
  3163. }
  3164. >
  3165. {fieldGuideActionLabel}
  3166. </Button>
  3167. </Space>
  3168. </div>
  3169. ))}
  3170. </div>
  3171. </Card>
  3172. ))}
  3173. </div>
  3174. )}
  3175. </Space>
  3176. </Modal>
  3177. </>
  3178. );
  3179. };
  3180. export default ParamOverrideEditorModal;