소스 검색

feat(routing): add panel-only comment field to routing rules (#6361)

Can annotate rules with a human-readable note for easier management.
The comment is stripped before sending the config to xray-core (same
pattern as the existing 'enabled' flag).

Backend: stripDisabledRules now removes 'comment' from generated config.
Frontend: input field in RuleFormModal, column in desktop table, chip
with tooltip in mobile card list. Schema and type definitions updated.
Sentiago 17 시간 전
부모
커밋
1bf078c51e

+ 30 - 0
frontend/src/pages/xray/routing/RoutingTab.css

@@ -14,6 +14,36 @@
   transition: opacity 0.15s;
 }
 
+.rule-comment-cell {
+  display: block;
+  max-width: 140px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: 12px;
+  color: var(--ant-color-text-tertiary);
+}
+
+.rule-comment {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  margin-top: 6px;
+  padding: 2px 6px;
+  font-size: 12px;
+  color: var(--ant-color-text-tertiary);
+  border-radius: 4px;
+  background: var(--ant-color-fill-tertiary);
+  max-width: 100%;
+  overflow: hidden;
+}
+
+.rule-comment-text {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
 .drag-handle:hover {
   opacity: 0.8;
 }

+ 1 - 0
frontend/src/pages/xray/routing/RoutingTab.tsx

@@ -89,6 +89,7 @@ export default function RoutingTab({
           if (rule.attrs && typeof rule.attrs === 'object' && !Array.isArray(rule.attrs)) {
             r.attrs = JSON.stringify(rule.attrs, null, 2);
           }
+          r.comment = rule.comment || undefined;
           r.outboundTag = rule.outboundTag;
           r.balancerTag = rule.balancerTag;
           return r;

+ 7 - 0
frontend/src/pages/xray/routing/RuleCardList.tsx

@@ -181,6 +181,13 @@ export default function RuleCardList({
                 ))}
               </div>
             )}
+            {rule.comment && (
+              <Tooltip title={rule.comment}>
+                <div className="rule-comment">
+                  <span className="rule-comment-text">{rule.comment}</span>
+                </div>
+              </Tooltip>
+            )}
           </div>
         ))
       )}

+ 8 - 0
frontend/src/pages/xray/routing/RuleFormModal.tsx

@@ -13,6 +13,7 @@ import { buildRemarkByTag, formatInboundTag, isApiRule } from './helpers';
 
 export interface RoutingRule {
   enabled?: boolean;
+  comment?: string;
   type?: string;
   domain?: string | string[];
   ip?: string | string[];
@@ -42,6 +43,7 @@ interface RuleFormModalProps {
 
 const initialForm = (): RuleFormValues => ({
   enabled: true,
+  comment: '',
   domain: '',
   ip: '',
   port: '',
@@ -104,6 +106,7 @@ export default function RuleFormModal({
     if (rule) {
       methods.reset({
         enabled: rule.enabled !== false,
+        comment: rule.comment || '',
         domain: Array.isArray(rule.domain) ? rule.domain.join(',') : rule.domain || '',
         ip: Array.isArray(rule.ip) ? rule.ip.join(',') : rule.ip || '',
         port: rule.port || '',
@@ -132,6 +135,7 @@ export default function RuleFormModal({
     const built: Record<string, unknown> = {
       type: 'field',
       enabled: v.enabled,
+      comment: v.comment,
       domain: csv(v.domain),
       ip: csv(v.ip),
       port: v.port,
@@ -185,6 +189,10 @@ export default function RuleFormModal({
             <Switch disabled={isApiRule(rule ?? {})} />
           </FormField>
 
+          <FormField name="comment" label={t('comment')}>
+            <Input maxLength={200} showCount placeholder={t('comment')} />
+          </FormField>
+
           <FormField
             name="sourceIP"
             label={

+ 1 - 0
frontend/src/pages/xray/routing/types.ts

@@ -1,6 +1,7 @@
 export interface RuleRow {
   key: number;
   enabled?: boolean;
+  comment?: string;
   domain?: string;
   ip?: string;
   port?: string;

+ 15 - 1
frontend/src/pages/xray/routing/useRoutingColumns.tsx

@@ -1,6 +1,6 @@
 import { useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Button, Dropdown, Switch, Tag } from 'antd';
+import { Button, Dropdown, Switch, Tag, Tooltip } from 'antd';
 import {
   MoreOutlined,
   EditOutlined,
@@ -193,6 +193,20 @@ export function useRoutingColumns({
           </div>
         ),
       },
+      {
+        title: t('comment'),
+        align: 'left',
+        width: 150,
+        key: 'comment',
+        render: (_v, record) =>
+          record.comment ? (
+            <Tooltip title={record.comment}>
+              <span className="rule-comment-cell">{record.comment}</span>
+            </Tooltip>
+          ) : (
+            <span className="criterion-empty">—</span>
+          ),
+      },
       {
         title: t('pages.inbounds.network'),
         align: 'left',

+ 1 - 0
frontend/src/schemas/routing.ts

@@ -15,6 +15,7 @@ export type RuleWebhook = z.infer<typeof RuleWebhookSchema>;
 export const RuleObjectSchema = z.object({
   type: z.literal('field').default('field'),
   enabled: z.boolean().optional(),
+  comment: z.string().optional(),
   domain: z.array(z.string()).optional(),
   ip: z.array(z.string()).optional(),
   port: PortValueSchema.optional(),

+ 1 - 0
frontend/src/schemas/xray.ts

@@ -110,6 +110,7 @@ export const OutboundTestResultListSchema = z.array(OutboundTestResultSchema);
 
 export const RuleFormSchema = z.object({
   enabled: z.boolean(),
+  comment: z.string(),
   domain: z.string(),
   ip: z.string(),
   port: z.string(),

+ 7 - 3
internal/web/service/xray.go

@@ -1060,9 +1060,9 @@ func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
 }
 
 // stripDisabledRules removes routing rules marked `enabled: false` from the
-// generated runtime config and strips the panel-only `enabled` key from the
-// rest, since xray-core has no such field. The internal api rule is always
-// kept (see isApiRule) so traffic stats can't be toggled off. The stored
+// generated runtime config and strips panel-only keys (`enabled`, `comment`)
+// from the rest, since xray-core has no such fields. The internal api rule is
+// always kept (see isApiRule) so traffic stats can't be toggled off. The stored
 // template is untouched — only the generated config is filtered.
 func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
 	if len(routerCfg) == 0 {
@@ -1097,6 +1097,10 @@ func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
 			delete(rule, "enabled")
 			changed = true
 		}
+		if _, exists := rule["comment"]; exists {
+			delete(rule, "comment")
+			changed = true
+		}
 		activeRules = append(activeRules, rule)
 	}