fix(setting):渠道代理改为系统配置而不是配置文件
This commit is contained in:
1 parent
3c749c6c0a
commit
4d0e2901d9
6 files changed
+325
-37
No files matched your search
@@ -873,3 +873,22 @@ s.Start()
|
||||
- **不要丢弃 `s.Start()` 的 error**:Start 失败后测试仍会继续跑,症状表现为断言数值不对或 EOF,与真正原因(服务器没起来)完全无关,极易误导排查方向。
|
||||
|
||||
排查信号:测试断言"期望 N 实际 0"且伴随 `EOF`;`GetListenedPort()` 返回 `-1`;同一测试在 Linux CI 通过但本地 Windows 失败。
|
||||
|
||||
### 2026-09-01(二):channel_proxy_url 双数据源分裂——设置页保存的代理对 relay 渠道转发不生效
|
||||
|
||||
**问题**:管理后台「系统设置 → 渠道配置 → 代理地址」保存后,渠道编辑中开启「使用代理」的请求并不走该代理。设置页把 `channel_proxy_url` 写入 `sys_options`,但 relay 层(`relay/common/http_client.go` 的 `GetSystemProxyURL`)读的是 `g.Cfg().MustGet(ctx, "channel_proxy_url")`——即 `manifest/config/config.yaml` 配置文件里的**同名 key**。两套数据源互不相通:设置页的值只被 `oauth/client.go`(OAuth 出站)读到,它宣称的主要用途(渠道转发代理、Realtime WebSocket 拨号)从未读到过。
|
||||
|
||||
**原因**:同一配置 key 被两个模块各自实现了读取逻辑,一个走设置注册表(`sys_options`),一个走 `g.Cfg()`(配置文件),没有收敛到同一加载入口。`g.Cfg().MustGet` 对键不存在不报错、静默返回空串(见 2026-08-31 记录),掩盖了分裂——渠道一直"代理未配置",无人察觉。
|
||||
|
||||
**修复**:
|
||||
|
||||
1. `relay/common/http_client.go`:`GetSystemProxyURL` 不再读 `g.Cfg()`(含 10s TTL 轮询缓存一并删除),改为读包内 atomic 值,新增 `SetSystemProxyURL` setter;
|
||||
2. `internal/cmd/cmd.go`:沿用 `SetGlobalRequestTimeoutSeconds` 的桥接注入模式(relay 不反向 import internal,会循环依赖)——启动时 + `OnSettingsChanged("channel_proxy_url")` 时从 `sys_options` 读取并注入,免重启、多实例经 Redis pub/sub 各自刷新;
|
||||
3. `config.example.yaml` 删除已失效的 `channel_proxy_url` key,配置文件不再承载该配置。
|
||||
|
||||
**正确做法(通用规则)**:
|
||||
|
||||
- **一个配置 key 只能有一个数据源、一个加载入口**。凡注册进设置注册表(`sys_options`,管理员可改)的 key,所有消费方——包括 relay 这类不 import internal 的独立模块——都必须经同一加载函数取值;独立模块用「setter 注入 + OnSettingsChanged 刷新」桥接(本项目既有模式:全局超时 `SetGlobalRequestTimeoutSeconds`)。
|
||||
- **禁止用 `g.Cfg()` 读取任何注册表里的 key**:同名 key 在配置文件里恰好存在时会造成"双真相源",配置文件的值会静默覆盖管理员在后台设置的值(或反之),且 `MustGet` 对键缺失不报错,分裂长期不可见。
|
||||
|
||||
排查信号:设置页某配置"保存了但不生效";`grep -rn "g.Cfg()"` 命中的 key 同时出现在 `settings_registry.go` 中。
|
||||
+13
-8
@@ -154,8 +154,8 @@ var (
|
||||
registerCronJobs(cs)
|
||||
cs.StartBackground(ctx)
|
||||
|
||||
// 全局超时配置注入 relay 层(渠道级 settings.timeout_seconds 仍优先,此处为未配置渠道的兜底)
|
||||
syncGlobalRelayTimeouts(ctx)
|
||||
// relay 层运行时配置注入(全局超时兜底/系统代理;渠道级 settings 仍优先)
|
||||
syncGlobalRelaySettings(ctx)
|
||||
|
||||
// 启动钱包物化器(boot goroutine,秒级刷新 Redis 权威钱包状态到 DB;不走 cron)
|
||||
billing.StartWalletMaterializer(ctx)
|
||||
@@ -385,22 +385,27 @@ func watchChannelAutoTestInterval(cs *common.CronScheduler) {
|
||||
})
|
||||
}
|
||||
|
||||
// syncGlobalRelayTimeouts 将系统设置中的全局超时注入 relay 层,并在配置变更时动态刷新(免重启)。
|
||||
// 优先级:渠道级 settings.timeout_seconds > 此处注入的全局值 > relay 层内置默认(180s/300s)。
|
||||
// syncGlobalRelaySettings 将系统设置中 relay 层的运行时配置注入 relay 层,并在配置变更时动态刷新(免重启):
|
||||
// - 全局超时:优先级 渠道级 settings.timeout_seconds > 此处注入的全局值 > relay 层内置默认(180s/300s)
|
||||
// - 系统代理:channel_proxy_url,渠道编辑中开启「使用代理」的请求经此代理转发
|
||||
//
|
||||
// relay 包不反向 import internal(会循环依赖),故由本函数桥接注入。
|
||||
func syncGlobalRelayTimeouts(ctx context.Context) {
|
||||
func syncGlobalRelaySettings(ctx context.Context) {
|
||||
apply := func(ctx context.Context) {
|
||||
relaycommon.SetGlobalRequestTimeoutSeconds(common.Config().GetInt(ctx, "request_timeout_seconds"))
|
||||
relaycommon.SetGlobalStreamIdleTimeoutSeconds(common.Config().GetInt(ctx, "streaming_timeout_seconds"))
|
||||
relaycommon.SetSystemProxyURL(common.Config().GetString(ctx, "channel_proxy_url"))
|
||||
}
|
||||
apply(ctx)
|
||||
common.OnSettingsChanged(func(ctx context.Context, key string) {
|
||||
if key != "request_timeout_seconds" && key != "streaming_timeout_seconds" {
|
||||
if key != "request_timeout_seconds" && key != "streaming_timeout_seconds" && key != "channel_proxy_url" {
|
||||
return
|
||||
}
|
||||
apply(ctx)
|
||||
g.Log().Infof(ctx, "全局 relay 超时已刷新: request=%ds stream_idle=%ds",
|
||||
relaycommon.GlobalRequestTimeoutSeconds(), relaycommon.GlobalStreamIdleTimeoutSeconds())
|
||||
// 代理 URL 可能含认证凭据,只记录是否已配置,不落日志
|
||||
g.Log().Infof(ctx, "全局 relay 配置已刷新: request=%ds stream_idle=%ds proxy_configured=%v",
|
||||
relaycommon.GlobalRequestTimeoutSeconds(), relaycommon.GlobalStreamIdleTimeoutSeconds(),
|
||||
relaycommon.GetSystemProxyURL() != "")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -202,11 +202,11 @@ var Registry = []SettingDef{
|
||||
{Key: "channel_auto_disable_enabled", Type: SettingTypeBool, Default: "false", Category: "channel",
|
||||
Label: "渠道自动禁用", Description: "渠道熔断持续超过路由策略 breaker.autoDisableAfterSeconds(默认 10 分钟)未恢复时自动禁用"},
|
||||
{Key: "channel_routing_policy", Type: SettingTypeJSON, Default: "", Category: "channel",
|
||||
Label: "路由策略覆盖(JSON)", Description: "渠道调度引擎策略,部分字段覆盖内置默认值,保存后 30 秒内热生效;留空使用全部默认。字段说明见 docs/reference/渠道调度运维手册.md"},
|
||||
Label: "路由策略覆盖(JSON)", Description: "渠道调度引擎策略,部分字段覆盖内置默认值,保存后 30 秒内热生效;留空使用全部默认。高级功能,非必要不要修改;字段说明见管理后台渠道配置页路由策略的说明弹窗"},
|
||||
{Key: "health_snapshot_retention_days", Type: SettingTypeInt, Default: "7", Category: "channel",
|
||||
Label: "健康快照保留天数", Validation: "min:1,max:90"},
|
||||
{Key: "channel_proxy_url", Type: SettingTypeString, Default: "", Category: "channel",
|
||||
Label: "代理地址", Description: "全局代理 URL,支持 http:// 和 socks5://,如 http://127.0.0.1:7890。启用代理的渠道会通过此代理转发请求"},
|
||||
Label: "代理地址", Description: "全局代理 URL,支持 http:// 和 socks5://,如 http://127.0.0.1:7890。启用代理的渠道会通过此代理转发请求;OAuth 出站请求同样走此代理。保存后即时生效,无需重启"},
|
||||
{Key: "sync_image_async_enabled", Type: SettingTypeBool, Default: "false", Category: "channel",
|
||||
Label: "同步图片厂商异步化", Description: "开启后,同步阻塞返回的图片厂商(如 OpenAI/DALL·E)走 /v1/images/generations/async 时由后台 worker 池异步处理,客户端提交即拿 task_id 后轮询取图;关闭则该端点对同步厂商返回不支持。依赖对象存储保存生成结果,开启前需先在「存储配置」中配置对象存储(OSS/S3/COS)"},
|
||||
{Key: "sync_image_rehost_url", Type: SettingTypeBool, Default: "false", Category: "channel",
|
||||
|
||||
@@ -83,9 +83,6 @@ demo:
|
||||
enabled: false
|
||||
message: "演示环境,数据不可修改"
|
||||
|
||||
# Channel proxy (for accessing Google/Anthropic etc. from China)
|
||||
channel_proxy_url: ""
|
||||
|
||||
# System update source override
|
||||
# Default: GitHub official API (https://api.github.com/repos/qianfree/team-api).
|
||||
# Override to point at a self-hosted release server / local mock release (see
|
||||
|
||||
+17
-21
@@ -62,33 +62,29 @@ var (
|
||||
|
||||
// proxiedState 管理带代理的 HTTP 客户端,按需初始化并在代理 URL 变更时重建。
|
||||
var proxiedState struct {
|
||||
mu sync.RWMutex
|
||||
proxyURL string
|
||||
transport *http.Transport // ResponseHeaderTimeout=defaultResponseHeaderTimeout(普通)
|
||||
longRun *http.Transport // ResponseHeaderTimeout=0(长耗时同步)
|
||||
nonStream *http.Client
|
||||
stream *http.Client
|
||||
proxyURLCached atomic.Value // string
|
||||
cacheTime atomic.Int64 // 上次读取配置的 unix 时间戳(秒)
|
||||
mu sync.RWMutex
|
||||
proxyURL string
|
||||
transport *http.Transport // ResponseHeaderTimeout=defaultResponseHeaderTimeout(普通)
|
||||
longRun *http.Transport // ResponseHeaderTimeout=0(长耗时同步)
|
||||
nonStream *http.Client
|
||||
stream *http.Client
|
||||
proxyURLValue atomic.Value // string,系统代理 URL(sys_options channel_proxy_url,由 internal 注入)
|
||||
}
|
||||
|
||||
const proxyCacheTTL = 10 // seconds
|
||||
// SetSystemProxyURL 注入系统代理 URL(sys_options 的 channel_proxy_url)。
|
||||
// relay 包不 import internal(会循环依赖),由 internal 侧在启动时与配置变更时
|
||||
// (OnSettingsChanged,免重启)注入,模式同 SetGlobalRequestTimeoutSeconds。
|
||||
func SetSystemProxyURL(proxyURL string) {
|
||||
proxiedState.proxyURLValue.Store(proxyURL)
|
||||
}
|
||||
|
||||
// GetSystemProxyURL 从系统配置读取 channel_proxy_url(带本地缓存)。
|
||||
// GetSystemProxyURL 返回系统代理 URL(sys_options 的 channel_proxy_url,空串=未配置)。
|
||||
// 导出供 WebSocket 拨号器等非 HTTP 客户端调用方使用。
|
||||
func GetSystemProxyURL() string {
|
||||
now := time.Now().Unix()
|
||||
last := proxiedState.cacheTime.Load()
|
||||
if now-last < proxyCacheTTL {
|
||||
if v, ok := proxiedState.proxyURLCached.Load().(string); ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := proxiedState.proxyURLValue.Load().(string); ok {
|
||||
return v
|
||||
}
|
||||
|
||||
proxyURL := g.Cfg().MustGet(context.Background(), "channel_proxy_url").String()
|
||||
proxiedState.proxyURLCached.Store(proxyURL)
|
||||
proxiedState.cacheTime.Store(now)
|
||||
return proxyURL
|
||||
return ""
|
||||
}
|
||||
|
||||
// buildProxiedTransport 构建一个带(可选)代理的传输层,ResponseHeaderTimeout 由 rht 决定。
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { IconExclamationCircle } from '@arco-design/web-vue/es/icon'
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
const showPolicyGuide = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -55,7 +58,10 @@ const values = useFormValues()
|
||||
|
||||
<!-- 路由策略 -->
|
||||
<div class="section">
|
||||
<div class="section-title">路由策略</div>
|
||||
<div class="section-title section-title-with-help">
|
||||
路由策略
|
||||
<IconExclamationCircle class="help-icon" :size="14" @click="showPolicyGuide = true" />
|
||||
</div>
|
||||
<AFormItem label="路由策略覆盖(JSON)">
|
||||
<ATextarea
|
||||
:model-value="values['channel_routing_policy'] as string"
|
||||
@@ -67,7 +73,8 @@ const values = useFormValues()
|
||||
</AFormItem>
|
||||
<div class="section-desc">
|
||||
渠道调度引擎策略,部分字段覆盖内置默认值,保存后 30 秒内热生效;留空使用全部默认。
|
||||
保存时会校验 JSON 合法性与取值范围。字段说明见 docs/reference/渠道调度运维手册.md。
|
||||
保存时会校验 JSON 合法性与取值范围。<b>此为高级功能,非必要不要修改</b>;
|
||||
点击标题旁的感叹号图标可查看字段说明与配置示例。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +90,7 @@ const values = useFormValues()
|
||||
/>
|
||||
</AFormItem>
|
||||
<div class="section-desc">
|
||||
全局代理 URL,支持 http:// 和 socks5:// 协议。在渠道编辑中开启"使用代理"即可生效。
|
||||
全局代理 URL,支持 http:// 和 socks5:// 协议。在渠道编辑中开启"使用代理"即可生效,保存后即时生效,无需重启。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,6 +114,138 @@ const values = useFormValues()
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 路由策略功能说明弹窗 -->
|
||||
<AModal v-model:visible="showPolicyGuide" title="路由策略覆盖说明" :width="720" :footer="false">
|
||||
<div class="policy-guide">
|
||||
<div class="guide-warning">
|
||||
<IconExclamationCircle :size="16" class="warning-icon" />
|
||||
<div>
|
||||
<b>高级功能,非必要不要修改。</b>
|
||||
此处参数直接影响线上渠道调度行为,内置默认值已适用于绝大多数场景,调整前请先理解各参数含义。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="guide-intro">
|
||||
这是渠道调度引擎的总参数配置:调度器如何挑选渠道、失败如何重试、何时熔断,均由这份 JSON 控制。
|
||||
留空 = 全部使用内置默认值;填写的字段会浅覆盖到默认值之上(只写要改的字段)。
|
||||
保存时按 Schema 校验,非法配置直接拒绝;保存后最长 30 秒内热生效,无需重启。
|
||||
</p>
|
||||
|
||||
<div class="guide-section">
|
||||
<div class="guide-title"><span class="guide-index">1</span>调度打分公式</div>
|
||||
<p>调度器为每个请求给候选渠道打分,按权重加权随机选择:</p>
|
||||
<p><code>W(c) = baseWeight × tierFactor × healthFactor × headroom^γ × costFactor × rampFactor × protoFactor</code></p>
|
||||
</div>
|
||||
|
||||
<div class="guide-section">
|
||||
<div class="guide-title"><span class="guide-index">2</span>可覆盖字段与默认值</div>
|
||||
<div class="policy-table-wrap">
|
||||
<table class="policy-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>字段组</th>
|
||||
<th>关键字段(默认值)</th>
|
||||
<th>作用</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>tierFactors</td>
|
||||
<td>primary 1.0 / secondary 0.15 / reserve 0.02</td>
|
||||
<td>渠道层级流量偏置:备用层级平时几乎不接流量,主级全挂时才扩组兜底</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>health</td>
|
||||
<td>alpha 2 / latRefMs 3000</td>
|
||||
<td>成功率指数(越大越惩罚低成功率渠道);延迟基准(超过开始降权)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>load</td>
|
||||
<td>gamma 2 / leaseSeconds 90</td>
|
||||
<td>余量指数(越满的渠道越少接新请求);并发租约时长</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>cost</td>
|
||||
<td>beta 0.5 / min 0.5 / max 2.0</td>
|
||||
<td>成本偏好(越大越偏向便宜渠道)及影响幅度上下限</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ramp</td>
|
||||
<td>windowSeconds 120 / floor 0.05</td>
|
||||
<td>新渠道、熔断恢复渠道的小流量爬坡</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>binding</td>
|
||||
<td>ttlSeconds 1800 等</td>
|
||||
<td>会话→渠道亲和绑定的有效期与守卫阈值</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>retry</td>
|
||||
<td>原地 2 / 换渠道 2 / 凭证轮换 1 次;总时限 30s</td>
|
||||
<td>失败重试预算、退避曲线(100~1000ms)、429 等待上限(2000ms)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>breaker</td>
|
||||
<td>渠道级 8 / 模型级 4 次;冷却 30~300s;autoDisableAfterSeconds 600</td>
|
||||
<td>熔断阈值与冷却;autoDisableAfterSeconds 即本页「渠道自动禁用」的持续时长阈值</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>session</td>
|
||||
<td>headerName X-Session-Id 等</td>
|
||||
<td>会话亲和的解析来源(Claude metadata.user_id、Responses previous_response_id)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>replay</td>
|
||||
<td>unsafeModes / safeModes</td>
|
||||
<td>禁止盲目重放的端点(默认图片/视频生成,防止重复扣费)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>proto</td>
|
||||
<td>responsesMismatch 0.25 / chatBridgeMismatch 0.75</td>
|
||||
<td>入站协议与渠道能力不匹配时的降权因子</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-section">
|
||||
<div class="guide-title"><span class="guide-index">3</span>书写规则</div>
|
||||
<ol class="guide-rules">
|
||||
<li><b>浅覆盖</b>:嵌套对象只覆盖写出的字段,如 <code>{ "breaker": { "failThreshold": 5 } }</code> 只改这一项,同组其余保持默认</li>
|
||||
<li><code>tierFactors</code> 按键合并:只写 <code>secondary</code> 不影响 <code>primary</code> / <code>reserve</code></li>
|
||||
<li>⚠️ 数组字段(<code>replay.unsafeModes</code> / <code>safeModes</code>)是<b>整体替换</b>:要改必须写全完整列表,漏一项等于把它移出保护</li>
|
||||
<li>字段名使用 camelCase(如 <code>latRefMs</code>,不是 <code>lat_ref_ms</code>)</li>
|
||||
<li>清空文本框 = 恢复全部内置默认(空串合法)</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="guide-section">
|
||||
<div class="guide-title"><span class="guide-index">4</span>校验规则(保存时拦截)</div>
|
||||
<p>
|
||||
常见拒绝原因:<code>tierFactors.primary</code> ≤ 0 或层级名不合法(仅 primary / secondary / reserve);
|
||||
<code>health.alpha</code> ≤ 0;<code>cost.min</code> > <code>cost.max</code>;重试预算为负;
|
||||
<code>backoffMaxMs</code> < <code>backoffBaseMs</code>;<code>cooldownMaxSeconds</code> < <code>cooldownSeconds</code>;proto 因子不在 (0,1]。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="guide-example">
|
||||
<div class="guide-example-title">配置示例</div>
|
||||
<pre><code>{
|
||||
"version": 1,
|
||||
"tierFactors": { "secondary": 0.3 },
|
||||
"breaker": { "failThreshold": 5, "autoDisableAfterSeconds": 300 },
|
||||
"retry": { "totalDeadlineSeconds": 45, "failoverBudget": 3 },
|
||||
"binding": { "ttlSeconds": 900 }
|
||||
}</code></pre>
|
||||
<p>
|
||||
含义:备用渠道流量偏置翻倍;60 秒窗口 5 次失败即熔断;熔断 5 分钟不恢复自动禁用;
|
||||
重试总时限放宽到 45 秒、最多换 3 个渠道;会话亲和绑定缩短到 15 分钟。
|
||||
</p>
|
||||
<p>再如 <code>{ "health": { "latRefMs": 1500 }, "cost": { "beta": 1.0 } }</code>:延迟超过 1.5 秒即开始降权、更偏向便宜渠道。</p>
|
||||
</div>
|
||||
</div>
|
||||
</AModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -118,4 +257,136 @@ const values = useFormValues()
|
||||
line-height: 1.6;
|
||||
margin-top: -8px;
|
||||
}
|
||||
.section-title-with-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.help-icon {
|
||||
color: var(--color-text-3);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.help-icon:hover {
|
||||
color: rgb(var(--arcoblue-6));
|
||||
}
|
||||
|
||||
/* 路由策略说明弹窗(样式惯例对齐 ChannelsPage 调度说明弹窗) */
|
||||
.policy-guide {
|
||||
color: var(--color-text-1);
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
max-height: 65vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 6px;
|
||||
}
|
||||
.policy-guide .guide-intro {
|
||||
margin: 0 0 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
.policy-guide .guide-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.policy-guide .guide-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.policy-guide .guide-index {
|
||||
display: inline-flex;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 22px;
|
||||
border-radius: 50%;
|
||||
color: rgb(var(--arcoblue-6));
|
||||
background: var(--color-primary-light-1);
|
||||
font-size: 12px;
|
||||
}
|
||||
.policy-guide p {
|
||||
margin: 0;
|
||||
}
|
||||
.policy-guide p + p {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.policy-guide code {
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
color: rgb(var(--arcoblue-6));
|
||||
background: var(--color-primary-light-1);
|
||||
}
|
||||
.guide-warning {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 6px;
|
||||
background: var(--color-warning-light-1);
|
||||
}
|
||||
.guide-warning .warning-icon {
|
||||
color: rgb(var(--warning-6));
|
||||
flex: 0 0 auto;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.guide-rules {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.guide-rules li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.policy-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.policy-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.policy-table th,
|
||||
.policy-table td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--color-fill-2);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
.policy-table th {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
background: var(--color-fill-1);
|
||||
}
|
||||
.policy-table td:first-child {
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
.policy-guide .guide-example {
|
||||
padding: 14px 16px;
|
||||
border-radius: 6px;
|
||||
background: var(--color-fill-2);
|
||||
}
|
||||
.policy-guide .guide-example-title {
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.policy-guide .guide-example p {
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.policy-guide pre {
|
||||
margin: 8px 0 0;
|
||||
padding: 12px 14px;
|
||||
border-radius: 6px;
|
||||
background: var(--color-fill-1);
|
||||
overflow-x: auto;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.policy-guide pre code {
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user