prompt_config.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. package securityaudit
  2. import (
  3. "context"
  4. "crypto/sha256"
  5. "encoding/hex"
  6. "encoding/json"
  7. "fmt"
  8. "sort"
  9. "strings"
  10. "time"
  11. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  12. )
  13. const (
  14. DefaultWorkerCount = 4
  15. MaxWorkerCount = 32
  16. DefaultQueueCapacity = 32768
  17. MaxQueueCapacity = 100000
  18. DefaultTimeoutMS = 3000
  19. MinTimeoutMS = 100
  20. MaxTimeoutMS = 30000
  21. DefaultInputLimit = 4000
  22. MinInputLimit = 128
  23. MaxInputLimit = 100000
  24. DefaultPayloadTTL = 30 * time.Minute
  25. )
  26. type SecretEncryptor interface {
  27. Encrypt(plaintext string) (string, error)
  28. Decrypt(ciphertext string) (string, error)
  29. }
  30. // ConfigStore is the injectable boundary between hot-path prompt auditing and
  31. // the concrete settings/PostgreSQL/Redis-backed configuration manager.
  32. type ConfigStore interface {
  33. Start(ctx context.Context) error
  34. Shutdown(ctx context.Context) error
  35. Active() (ActiveConfig, bool)
  36. EffectiveMode() Mode
  37. // BlockingActivationDegraded is true when storage intent requires blocking
  38. // but no usable blocking snapshot is active (cold start or failed reload).
  39. // It must stay false when blocking is not intended, even if config is
  40. // untrusted—otherwise default-off deployments fail closed for all traffic.
  41. BlockingActivationDegraded() bool
  42. Public() (PublicConfig, error)
  43. Save(ctx context.Context, req UpdateConfigRequest, actorID int64) (PublicConfig, error)
  44. RuntimeState() (expected int64, active int64, loadedAt *time.Time, loadError string)
  45. Encrypt(value string) (string, error)
  46. Decrypt(value string) (string, error)
  47. }
  48. type StorageEndpoint struct {
  49. ID string `json:"id"`
  50. Name string `json:"name"`
  51. Protocol string `json:"protocol"`
  52. BaseURL string `json:"base_url"`
  53. Model string `json:"model"`
  54. TokenCiphertext string `json:"token_ciphertext,omitempty"`
  55. TimeoutMS int `json:"timeout_ms"`
  56. InputLimit int `json:"input_limit"`
  57. Enabled bool `json:"enabled"`
  58. }
  59. type storageConfig struct {
  60. Enabled bool `json:"enabled"`
  61. BlockingEnabled bool `json:"blocking_enabled"`
  62. BlockingLatestTurnOnly bool `json:"blocking_latest_turn_only"`
  63. StorePassEvents bool `json:"store_pass_events"`
  64. Strategy string `json:"strategy"`
  65. WorkerCount int `json:"worker_count"`
  66. QueueCapacity int `json:"queue_capacity"`
  67. Scanners []string `json:"scanners"`
  68. AllGroups bool `json:"all_groups"`
  69. GroupIDs []int64 `json:"group_ids"`
  70. Endpoints []StorageEndpoint `json:"endpoints"`
  71. ConfigVersion int64 `json:"config_version"`
  72. UpdatedAt time.Time `json:"updated_at"`
  73. UpdatedBy int64 `json:"updated_by"`
  74. ChangeSummary string `json:"change_summary"`
  75. }
  76. type ActiveEndpoint struct {
  77. ID string
  78. Name string
  79. Protocol string
  80. BaseURL string
  81. Model string
  82. Token string
  83. TimeoutMS int
  84. InputLimit int
  85. Enabled bool
  86. // TokenInvalid marks an endpoint whose persisted token ciphertext cannot be
  87. // decrypted with the current encryption key (key changed or auto-generated
  88. // on restart). The endpoint is kept visible for admins but excluded from
  89. // runtime use until the token is re-entered or cleared (issue #4887).
  90. TokenInvalid bool
  91. }
  92. type ActiveConfig struct {
  93. RiskControlEnabled bool
  94. Enabled bool
  95. BlockingEnabled bool
  96. BlockingLatestTurnOnly bool
  97. StorePassEvents bool
  98. Strategy string
  99. WorkerCount int
  100. QueueCapacity int
  101. Scanners []string
  102. AllGroups bool
  103. GroupIDs []int64
  104. Endpoints []ActiveEndpoint
  105. ConfigVersion int64
  106. UpdatedAt time.Time
  107. UpdatedBy int64
  108. ChangeSummary string
  109. }
  110. type PublicEndpoint struct {
  111. ID string `json:"id"`
  112. Name string `json:"name"`
  113. Protocol string `json:"protocol"`
  114. BaseURL string `json:"base_url"`
  115. Model string `json:"model"`
  116. TimeoutMS int `json:"timeout_ms"`
  117. InputLimit int `json:"input_limit"`
  118. Enabled bool `json:"enabled"`
  119. HasToken bool `json:"has_token"`
  120. TokenStatus string `json:"token_status"`
  121. }
  122. type PublicConfig struct {
  123. Enabled bool `json:"enabled"`
  124. BlockingEnabled bool `json:"blocking_enabled"`
  125. BlockingLatestTurnOnly bool `json:"blocking_latest_turn_only"`
  126. StorePassEvents bool `json:"store_pass_events"`
  127. EffectiveMode Mode `json:"effective_mode"`
  128. Strategy string `json:"strategy"`
  129. WorkerCount int `json:"worker_count"`
  130. QueueCapacity int `json:"queue_capacity"`
  131. Scanners []string `json:"scanners"`
  132. AllGroups bool `json:"all_groups"`
  133. GroupIDs []int64 `json:"group_ids"`
  134. Endpoints []PublicEndpoint `json:"endpoints"`
  135. ConfigVersion int64 `json:"config_version"`
  136. UpdatedAt time.Time `json:"updated_at"`
  137. UpdatedBy int64 `json:"updated_by"`
  138. ChangeSummary string `json:"change_summary"`
  139. }
  140. type UpdateEndpoint struct {
  141. ID string `json:"id" binding:"required"`
  142. Name string `json:"name" binding:"required"`
  143. Protocol string `json:"protocol"`
  144. BaseURL string `json:"base_url" binding:"required"`
  145. Model string `json:"model"`
  146. Token string `json:"token,omitempty"`
  147. ClearToken bool `json:"clear_token"`
  148. TimeoutMS int `json:"timeout_ms"`
  149. InputLimit int `json:"input_limit"`
  150. Enabled bool `json:"enabled"`
  151. }
  152. type UpdateConfigRequest struct {
  153. ExpectedConfigVersion int64 `json:"expected_config_version" binding:"required"`
  154. Enabled bool `json:"enabled"`
  155. BlockingEnabled bool `json:"blocking_enabled"`
  156. BlockingLatestTurnOnly bool `json:"blocking_latest_turn_only"`
  157. StorePassEvents bool `json:"store_pass_events"`
  158. Strategy string `json:"strategy"`
  159. WorkerCount int `json:"worker_count"`
  160. QueueCapacity int `json:"queue_capacity"`
  161. Scanners []string `json:"scanners"`
  162. AllGroups bool `json:"all_groups"`
  163. GroupIDs []int64 `json:"group_ids"`
  164. Endpoints []UpdateEndpoint `json:"endpoints"`
  165. }
  166. func DefaultStorageConfig() storageConfig {
  167. return storageConfig{
  168. Enabled: false,
  169. BlockingEnabled: false,
  170. BlockingLatestTurnOnly: false,
  171. StorePassEvents: false,
  172. Strategy: "priority",
  173. WorkerCount: DefaultWorkerCount,
  174. QueueCapacity: DefaultQueueCapacity,
  175. Scanners: append([]string(nil), AllScannerIDs...),
  176. AllGroups: true,
  177. GroupIDs: []int64{},
  178. Endpoints: []StorageEndpoint{},
  179. ConfigVersion: 1,
  180. }
  181. }
  182. func ParseStorageConfig(raw string) (storageConfig, error) {
  183. cfg := DefaultStorageConfig()
  184. if strings.TrimSpace(raw) == "" {
  185. return cfg, nil
  186. }
  187. if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
  188. return storageConfig{}, fmt.Errorf("decode prompt audit config: %w", err)
  189. }
  190. normalizeStorageConfig(&cfg)
  191. if err := validateStorageConfig(cfg); err != nil {
  192. return storageConfig{}, err
  193. }
  194. return cfg, nil
  195. }
  196. func normalizeStorageConfig(cfg *storageConfig) {
  197. if cfg == nil {
  198. return
  199. }
  200. if cfg.ConfigVersion < 1 {
  201. cfg.ConfigVersion = 1
  202. }
  203. if strings.TrimSpace(cfg.Strategy) == "" {
  204. cfg.Strategy = "priority"
  205. }
  206. if cfg.WorkerCount == 0 {
  207. cfg.WorkerCount = DefaultWorkerCount
  208. }
  209. if cfg.QueueCapacity == 0 {
  210. cfg.QueueCapacity = DefaultQueueCapacity
  211. }
  212. if len(cfg.Scanners) == 0 {
  213. cfg.Scanners = append([]string(nil), AllScannerIDs...)
  214. }
  215. cfg.Scanners = canonicalScannerIDs(cfg.Scanners)
  216. cfg.GroupIDs = canonicalInt64s(cfg.GroupIDs)
  217. // Preserve an invalid blocking-without-audit combination so validation can
  218. // reject it instead of silently changing administrator intent.
  219. for i := range cfg.Endpoints {
  220. ep := &cfg.Endpoints[i]
  221. ep.ID = strings.TrimSpace(ep.ID)
  222. ep.Name = strings.TrimSpace(ep.Name)
  223. ep.Protocol = strings.TrimSpace(ep.Protocol)
  224. if ep.Protocol == "" {
  225. ep.Protocol = "openai_compatible"
  226. }
  227. ep.BaseURL = strings.TrimSpace(ep.BaseURL)
  228. ep.Model = strings.TrimSpace(ep.Model)
  229. if ep.Model == "" {
  230. ep.Model = DefaultGuardModel
  231. }
  232. if ep.TimeoutMS == 0 {
  233. ep.TimeoutMS = DefaultTimeoutMS
  234. }
  235. if ep.InputLimit == 0 {
  236. ep.InputLimit = DefaultInputLimit
  237. }
  238. }
  239. }
  240. func validateStorageConfig(cfg storageConfig) error {
  241. if cfg.BlockingEnabled && !cfg.Enabled {
  242. return infraerrors.BadRequest(ErrorCodeRequiresEnabled, "开启同步阻止前必须先启用提示词审计")
  243. }
  244. if cfg.Strategy != "priority" {
  245. return infraerrors.BadRequest("prompt_audit_invalid_strategy", "提示词审计策略仅支持 priority")
  246. }
  247. if cfg.WorkerCount < 1 || cfg.WorkerCount > MaxWorkerCount {
  248. return infraerrors.BadRequest("prompt_audit_invalid_worker_count", "Worker 数量超出允许范围")
  249. }
  250. if cfg.QueueCapacity < 1 || cfg.QueueCapacity > MaxQueueCapacity {
  251. return infraerrors.BadRequest("prompt_audit_invalid_queue_capacity", "队列容量超出允许范围")
  252. }
  253. if !cfg.AllGroups && len(cfg.GroupIDs) == 0 {
  254. return infraerrors.BadRequest("prompt_audit_groups_required", "指定分组模式至少需要选择一个分组")
  255. }
  256. if len(cfg.Scanners) == 0 {
  257. return infraerrors.BadRequest("prompt_audit_scanners_required", "至少需要启用一个风险分类")
  258. }
  259. seen := make(map[string]struct{}, len(cfg.Endpoints))
  260. enabled := 0
  261. for _, ep := range cfg.Endpoints {
  262. if ep.ID == "" || ep.Name == "" {
  263. return infraerrors.BadRequest("prompt_audit_invalid_endpoint", "审计节点 ID 和名称不能为空")
  264. }
  265. if _, ok := seen[ep.ID]; ok {
  266. return infraerrors.BadRequest("prompt_audit_duplicate_endpoint", "审计节点 ID 不能重复")
  267. }
  268. seen[ep.ID] = struct{}{}
  269. if ep.Protocol != "openai_compatible" {
  270. return infraerrors.BadRequest("prompt_audit_invalid_endpoint_protocol", "审计节点仅支持 OpenAI 兼容协议")
  271. }
  272. if _, err := NormalizeBaseURL(ep.BaseURL); err != nil {
  273. return err
  274. }
  275. if ep.TimeoutMS < MinTimeoutMS || ep.TimeoutMS > MaxTimeoutMS {
  276. return infraerrors.BadRequest("prompt_audit_invalid_timeout", "审计节点超时超出允许范围")
  277. }
  278. if ep.InputLimit < MinInputLimit || ep.InputLimit > MaxInputLimit {
  279. return infraerrors.BadRequest("prompt_audit_invalid_input_limit", "审计节点输入上限超出允许范围")
  280. }
  281. if ep.Enabled {
  282. enabled++
  283. }
  284. }
  285. if cfg.Enabled && enabled == 0 {
  286. return infraerrors.BadRequest("prompt_audit_endpoint_required", "启用提示词审计前至少需要启用一个审计节点")
  287. }
  288. return nil
  289. }
  290. func validateUpdateConfigRequest(req UpdateConfigRequest) error {
  291. if strings.TrimSpace(req.Strategy) != "priority" {
  292. return infraerrors.BadRequest("prompt_audit_invalid_strategy", "提示词审计策略仅支持 priority")
  293. }
  294. if req.WorkerCount < 1 || req.WorkerCount > MaxWorkerCount {
  295. return infraerrors.BadRequest("prompt_audit_invalid_worker_count", "Worker 数量超出允许范围")
  296. }
  297. if req.QueueCapacity < 1 || req.QueueCapacity > MaxQueueCapacity {
  298. return infraerrors.BadRequest("prompt_audit_invalid_queue_capacity", "队列容量超出允许范围")
  299. }
  300. if len(req.Scanners) == 0 {
  301. return infraerrors.BadRequest("prompt_audit_scanners_required", "至少需要启用一个风险分类")
  302. }
  303. for _, scanner := range req.Scanners {
  304. if _, ok := ScannerCatalog[NormalizeCategory(scanner)]; !ok {
  305. return infraerrors.BadRequest("prompt_audit_invalid_scanner", "提示词审计风险分类无效")
  306. }
  307. }
  308. if !req.AllGroups {
  309. if len(req.GroupIDs) == 0 {
  310. return infraerrors.BadRequest("prompt_audit_groups_required", "指定分组模式至少需要选择一个分组")
  311. }
  312. for _, groupID := range req.GroupIDs {
  313. if groupID <= 0 {
  314. return infraerrors.BadRequest("prompt_audit_invalid_group", "提示词审计分组 ID 无效")
  315. }
  316. }
  317. }
  318. for _, endpoint := range req.Endpoints {
  319. if endpoint.TimeoutMS < MinTimeoutMS || endpoint.TimeoutMS > MaxTimeoutMS {
  320. return infraerrors.BadRequest("prompt_audit_invalid_timeout", "审计节点超时超出允许范围")
  321. }
  322. if endpoint.InputLimit < MinInputLimit || endpoint.InputLimit > MaxInputLimit {
  323. return infraerrors.BadRequest("prompt_audit_invalid_input_limit", "审计节点输入上限超出允许范围")
  324. }
  325. }
  326. return nil
  327. }
  328. func (cfg ActiveConfig) EffectiveMode() Mode {
  329. if !cfg.RiskControlEnabled || !cfg.Enabled {
  330. return ModeOff
  331. }
  332. if cfg.BlockingEnabled {
  333. return ModeBlocking
  334. }
  335. return ModeAsync
  336. }
  337. func (cfg ActiveConfig) IncludesGroup(groupID *int64) bool {
  338. if cfg.AllGroups {
  339. return true
  340. }
  341. if groupID == nil {
  342. return false
  343. }
  344. i := sort.Search(len(cfg.GroupIDs), func(i int) bool { return cfg.GroupIDs[i] >= *groupID })
  345. return i < len(cfg.GroupIDs) && cfg.GroupIDs[i] == *groupID
  346. }
  347. func (cfg ActiveConfig) EnabledEndpoints() []ActiveEndpoint {
  348. result := make([]ActiveEndpoint, 0, len(cfg.Endpoints))
  349. for _, ep := range cfg.Endpoints {
  350. if ep.Enabled {
  351. result = append(result, ep)
  352. }
  353. }
  354. return result
  355. }
  356. // InvalidTokenEndpointIDs lists endpoints whose stored token could not be
  357. // decrypted with the current encryption key.
  358. func (cfg ActiveConfig) InvalidTokenEndpointIDs() []string {
  359. ids := make([]string, 0)
  360. for _, ep := range cfg.Endpoints {
  361. if ep.TokenInvalid {
  362. ids = append(ids, ep.ID)
  363. }
  364. }
  365. return ids
  366. }
  367. func PublicFromStorage(cfg storageConfig, riskControlEnabled bool, invalidTokenEndpointIDs []string) PublicConfig {
  368. invalid := make(map[string]struct{}, len(invalidTokenEndpointIDs))
  369. for _, id := range invalidTokenEndpointIDs {
  370. invalid[id] = struct{}{}
  371. }
  372. scanners := append([]string{}, cfg.Scanners...)
  373. groupIDs := append([]int64{}, cfg.GroupIDs...)
  374. endpoints := make([]PublicEndpoint, 0, len(cfg.Endpoints))
  375. for _, ep := range cfg.Endpoints {
  376. hasToken := strings.TrimSpace(ep.TokenCiphertext) != ""
  377. status := "missing"
  378. if hasToken {
  379. status = "configured"
  380. if _, ok := invalid[ep.ID]; ok {
  381. status = "invalid"
  382. }
  383. }
  384. endpoints = append(endpoints, PublicEndpoint{
  385. ID: ep.ID, Name: ep.Name, Protocol: ep.Protocol, BaseURL: ep.BaseURL,
  386. Model: ep.Model, TimeoutMS: ep.TimeoutMS, InputLimit: ep.InputLimit,
  387. Enabled: ep.Enabled, HasToken: hasToken, TokenStatus: status,
  388. })
  389. }
  390. active := ActiveConfig{RiskControlEnabled: riskControlEnabled, Enabled: cfg.Enabled, BlockingEnabled: cfg.BlockingEnabled}
  391. return PublicConfig{
  392. Enabled: cfg.Enabled, BlockingEnabled: cfg.BlockingEnabled, BlockingLatestTurnOnly: cfg.BlockingLatestTurnOnly, StorePassEvents: cfg.StorePassEvents,
  393. EffectiveMode: active.EffectiveMode(), Strategy: cfg.Strategy, WorkerCount: cfg.WorkerCount,
  394. QueueCapacity: cfg.QueueCapacity, Scanners: scanners, AllGroups: cfg.AllGroups,
  395. GroupIDs: groupIDs, Endpoints: endpoints, ConfigVersion: cfg.ConfigVersion,
  396. UpdatedAt: cfg.UpdatedAt, UpdatedBy: cfg.UpdatedBy, ChangeSummary: cfg.ChangeSummary,
  397. }
  398. }
  399. func ActiveFromStorage(cfg storageConfig, riskControlEnabled bool, encryptor SecretEncryptor) (ActiveConfig, error) {
  400. active := ActiveConfig{
  401. RiskControlEnabled: riskControlEnabled, Enabled: cfg.Enabled, BlockingEnabled: cfg.BlockingEnabled,
  402. BlockingLatestTurnOnly: cfg.BlockingLatestTurnOnly,
  403. StorePassEvents: cfg.StorePassEvents, Strategy: cfg.Strategy, WorkerCount: cfg.WorkerCount,
  404. QueueCapacity: cfg.QueueCapacity, Scanners: append([]string(nil), cfg.Scanners...), AllGroups: cfg.AllGroups,
  405. GroupIDs: append([]int64(nil), cfg.GroupIDs...), ConfigVersion: cfg.ConfigVersion,
  406. UpdatedAt: cfg.UpdatedAt, UpdatedBy: cfg.UpdatedBy, ChangeSummary: cfg.ChangeSummary,
  407. Endpoints: make([]ActiveEndpoint, 0, len(cfg.Endpoints)),
  408. }
  409. for _, ep := range cfg.Endpoints {
  410. token := ""
  411. tokenInvalid := false
  412. if ep.TokenCiphertext != "" {
  413. if encryptor == nil {
  414. return ActiveConfig{}, fmt.Errorf("prompt audit secret encryptor unavailable")
  415. }
  416. plain, err := encryptor.Decrypt(ep.TokenCiphertext)
  417. if err != nil {
  418. // An undecryptable token (encryption key changed or regenerated)
  419. // must not take the whole config down: admins would otherwise be
  420. // locked out of the real config version and unable to recover
  421. // (issue #4887). Keep the ciphertext persisted, but exclude the
  422. // endpoint from runtime use until the token is re-entered.
  423. tokenInvalid = true
  424. } else {
  425. token = plain
  426. }
  427. }
  428. active.Endpoints = append(active.Endpoints, ActiveEndpoint{
  429. ID: ep.ID, Name: ep.Name, Protocol: ep.Protocol, BaseURL: ep.BaseURL, Model: ep.Model,
  430. Token: token, TimeoutMS: ep.TimeoutMS, InputLimit: ep.InputLimit,
  431. Enabled: ep.Enabled && !tokenInvalid, TokenInvalid: tokenInvalid,
  432. })
  433. }
  434. return active, nil
  435. }
  436. func changeSummary(cfg storageConfig) string {
  437. summary := struct {
  438. Enabled bool `json:"enabled"`
  439. BlockingEnabled bool `json:"blocking_enabled"`
  440. BlockingLatestTurnOnly bool `json:"blocking_latest_turn_only"`
  441. StorePassEvents bool `json:"store_pass_events"`
  442. EndpointCount int `json:"endpoint_count"`
  443. ScannerCount int `json:"scanner_count"`
  444. AllGroups bool `json:"all_groups"`
  445. GroupCount int `json:"group_count"`
  446. GroupHash string `json:"group_hash"`
  447. }{cfg.Enabled, cfg.BlockingEnabled, cfg.BlockingLatestTurnOnly, cfg.StorePassEvents, len(cfg.Endpoints), len(cfg.Scanners), cfg.AllGroups, len(cfg.GroupIDs), ""}
  448. rawGroups, _ := json.Marshal(cfg.GroupIDs)
  449. digest := sha256.Sum256(rawGroups)
  450. summary.GroupHash = hex.EncodeToString(digest[:])
  451. raw, _ := json.Marshal(summary)
  452. return string(raw)
  453. }
  454. func canonicalInt64s(values []int64) []int64 {
  455. seen := make(map[int64]struct{}, len(values))
  456. result := make([]int64, 0, len(values))
  457. for _, value := range values {
  458. if value <= 0 {
  459. continue
  460. }
  461. if _, ok := seen[value]; ok {
  462. continue
  463. }
  464. seen[value] = struct{}{}
  465. result = append(result, value)
  466. }
  467. sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
  468. return result
  469. }
  470. func canonicalScannerIDs(values []string) []string {
  471. seen := make(map[string]struct{}, len(values))
  472. for _, value := range values {
  473. id := NormalizeCategory(value)
  474. if _, ok := ScannerCatalog[id]; ok {
  475. seen[id] = struct{}{}
  476. }
  477. }
  478. result := make([]string, 0, len(seen))
  479. for _, id := range AllScannerIDs {
  480. if _, ok := seen[id]; ok {
  481. result = append(result, id)
  482. }
  483. }
  484. return result
  485. }