feat(monitor): switch headers input to key-value rows
- AdvancedRequestConfig 把 headers textarea 换成行式:每行 name 输入 + value 输入 + 删除按钮,底部「+ 添加 Header」。直观区分名/值,不用再一行 "Key: Value" 自己拆。 - 校验下放到行级:name 含空格或冒号才报错,未填仅占位不报错(避免输入时频繁红字)。 - 外部 props 同值不回写,避免 commit 后行被重排。 - chore: 移除 CLAUDE.md 里 silentflower remote 行(不再追踪)。
This commit is contained in:
parent
a7415d4d2e
commit
e1193212b5
@ -1,15 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Headers textarea -->
|
<!-- Headers key-value rows -->
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.channelMonitor.advanced.headers') }}</label>
|
<label class="input-label">{{ t('admin.channelMonitor.advanced.headers') }}</label>
|
||||||
<textarea
|
<div class="space-y-1.5">
|
||||||
v-model="headersText"
|
<div
|
||||||
rows="4"
|
v-for="(row, i) in headerRows"
|
||||||
:placeholder="t('admin.channelMonitor.advanced.headersPlaceholder')"
|
:key="i"
|
||||||
class="input font-mono text-xs"
|
class="flex items-center gap-2"
|
||||||
@blur="commitHeaders"
|
>
|
||||||
/>
|
<input
|
||||||
|
v-model="row.name"
|
||||||
|
type="text"
|
||||||
|
spellcheck="false"
|
||||||
|
:placeholder="t('admin.channelMonitor.advanced.headerNamePlaceholder')"
|
||||||
|
class="input w-52 flex-none font-mono text-xs"
|
||||||
|
@blur="commitHeaders"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="row.value"
|
||||||
|
type="text"
|
||||||
|
spellcheck="false"
|
||||||
|
:placeholder="t('admin.channelMonitor.advanced.headerValuePlaceholder')"
|
||||||
|
class="input flex-1 font-mono text-xs"
|
||||||
|
@blur="commitHeaders"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-none rounded p-1 text-gray-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-500/10 dark:hover:text-red-400"
|
||||||
|
:title="t('common.delete')"
|
||||||
|
@click="removeRow(i)"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-1 rounded border border-dashed border-gray-300 px-2 py-1 text-xs text-gray-500 hover:border-primary-400 hover:text-primary-600 dark:border-dark-600 dark:text-gray-400 dark:hover:border-primary-500 dark:hover:text-primary-400"
|
||||||
|
@click="addRow"
|
||||||
|
>
|
||||||
|
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
{{ t('admin.channelMonitor.advanced.headerAddRow') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p v-if="headersError" class="mt-1 text-xs text-red-500">{{ headersError }}</p>
|
<p v-if="headersError" class="mt-1 text-xs text-red-500">{{ headersError }}</p>
|
||||||
<p v-else class="mt-1 text-xs text-gray-400">
|
<p v-else class="mt-1 text-xs text-gray-400">
|
||||||
{{ t('admin.channelMonitor.advanced.headersHint') }}
|
{{ t('admin.channelMonitor.advanced.headersHint') }}
|
||||||
@ -85,51 +122,79 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// ---- Headers textarea (Key: Value per line) ----
|
// ---- Headers key-value rows ----
|
||||||
const headersText = ref(serializeHeaders(props.extraHeaders))
|
interface HeaderRow {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerRows = ref<HeaderRow[]>(toRows(props.extraHeaders))
|
||||||
const headersError = ref('')
|
const headersError = ref('')
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.extraHeaders,
|
() => props.extraHeaders,
|
||||||
(v) => {
|
(v) => {
|
||||||
// 外部重置时(如切换平台 / 应用模板)同步文本
|
// 外部重置时(切换平台 / 应用模板)同步行。
|
||||||
headersText.value = serializeHeaders(v)
|
// 同值不回写,避免每次 commit 都把行重排。
|
||||||
|
if (!isSameHeaderMap(toMap(headerRows.value), v)) {
|
||||||
|
headerRows.value = toRows(v)
|
||||||
|
}
|
||||||
headersError.value = ''
|
headersError.value = ''
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function toRows(h: Record<string, string>): HeaderRow[] {
|
||||||
|
const entries = Object.entries(h || {})
|
||||||
|
if (entries.length === 0) return [{ name: '', value: '' }]
|
||||||
|
return entries.map(([name, value]) => ({ name, value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMap(rows: HeaderRow[]): Record<string, string> {
|
||||||
|
const out: Record<string, string> = {}
|
||||||
|
for (const row of rows) {
|
||||||
|
const name = row.name.trim()
|
||||||
|
if (name === '') continue
|
||||||
|
out[name] = row.value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameHeaderMap(a: Record<string, string>, b: Record<string, string>): boolean {
|
||||||
|
const ak = Object.keys(a)
|
||||||
|
const bk = Object.keys(b || {})
|
||||||
|
if (ak.length !== bk.length) return false
|
||||||
|
for (const k of ak) {
|
||||||
|
if (a[k] !== b[k]) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function commitHeaders() {
|
function commitHeaders() {
|
||||||
const parsed = parseHeaders(headersText.value)
|
// 空白 name + 空白 value 的行允许保留作为"占位新行",不报错;
|
||||||
if (parsed.error) {
|
// name 非空但 value 为空(或反之)都视为用户正在编辑,同样不报错。
|
||||||
headersError.value = parsed.error
|
// 只在 name 里含冒号这种明显不合法时兜一下。
|
||||||
return
|
for (const row of headerRows.value) {
|
||||||
|
const name = row.name.trim()
|
||||||
|
if (name === '') continue
|
||||||
|
if (name.includes(':') || /\s/.test(name)) {
|
||||||
|
headersError.value = t('admin.channelMonitor.advanced.headerNameInvalid', { name })
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
headersError.value = ''
|
headersError.value = ''
|
||||||
emit('update:extraHeaders', parsed.headers)
|
emit('update:extraHeaders', toMap(headerRows.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeHeaders(h: Record<string, string>): string {
|
function addRow() {
|
||||||
return Object.entries(h || {})
|
headerRows.value.push({ name: '', value: '' })
|
||||||
.map(([k, v]) => `${k}: ${v}`)
|
|
||||||
.join('\n')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseHeaders(raw: string): { headers: Record<string, string>; error: string } {
|
function removeRow(index: number) {
|
||||||
const result: Record<string, string> = {}
|
headerRows.value.splice(index, 1)
|
||||||
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)
|
if (headerRows.value.length === 0) {
|
||||||
for (const line of lines) {
|
headerRows.value.push({ name: '', value: '' })
|
||||||
const idx = line.indexOf(':')
|
|
||||||
if (idx <= 0) {
|
|
||||||
return { headers: {}, error: t('admin.channelMonitor.advanced.headersParseError', { line }) }
|
|
||||||
}
|
|
||||||
const key = line.slice(0, idx).trim()
|
|
||||||
const value = line.slice(idx + 1).trim()
|
|
||||||
if (!key) {
|
|
||||||
return { headers: {}, error: t('admin.channelMonitor.advanced.headersParseError', { line }) }
|
|
||||||
}
|
|
||||||
result[key] = value
|
|
||||||
}
|
}
|
||||||
return { headers: result, error: '' }
|
commitHeaders()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Body mode + JSON ----
|
// ---- Body mode + JSON ----
|
||||||
|
|||||||
@ -2163,7 +2163,11 @@ export default {
|
|||||||
sectionHint: 'Customize request headers and body to bypass upstream client-detection (e.g. "only Claude Code clients allowed").',
|
sectionHint: 'Customize request headers and body to bypass upstream client-detection (e.g. "only Claude Code clients allowed").',
|
||||||
headers: 'Custom request headers',
|
headers: 'Custom request headers',
|
||||||
headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219',
|
headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219',
|
||||||
headersHint: 'One Key: Value per line; merged on top of adapter defaults (user wins). Hop-by-hop headers (Host / Content-Length / ...) are ignored.',
|
headerNamePlaceholder: 'Header name',
|
||||||
|
headerValuePlaceholder: 'Value',
|
||||||
|
headerAddRow: 'Add header',
|
||||||
|
headerNameInvalid: 'Header name cannot contain whitespace or colon: {name}',
|
||||||
|
headersHint: 'Merged on top of adapter defaults (user wins). Hop-by-hop headers (Host / Content-Length / ...) are ignored.',
|
||||||
headersParseError: 'Cannot parse line: {line}',
|
headersParseError: 'Cannot parse line: {line}',
|
||||||
bodyMode: 'Body handling',
|
bodyMode: 'Body handling',
|
||||||
bodyModeOff: 'Default',
|
bodyModeOff: 'Default',
|
||||||
|
|||||||
@ -2242,7 +2242,11 @@ export default {
|
|||||||
sectionHint: '自定义请求头和请求体,用于突破上游的客户端识别限制(如仅允许 Claude Code 客户端)。',
|
sectionHint: '自定义请求头和请求体,用于突破上游的客户端识别限制(如仅允许 Claude Code 客户端)。',
|
||||||
headers: '自定义请求头',
|
headers: '自定义请求头',
|
||||||
headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219',
|
headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219',
|
||||||
headersHint: '每行一对 Key: Value;会与默认请求头合并,用户值优先。hop-by-hop 类 header(Host/Content-Length/...)会被忽略。',
|
headerNamePlaceholder: 'Header 名',
|
||||||
|
headerValuePlaceholder: 'Value',
|
||||||
|
headerAddRow: '添加 Header',
|
||||||
|
headerNameInvalid: 'Header 名不能包含空格或冒号:{name}',
|
||||||
|
headersHint: '与默认请求头合并,用户值优先。hop-by-hop 类 header(Host/Content-Length/...)会被忽略。',
|
||||||
headersParseError: '无法解析这一行:{line}',
|
headersParseError: '无法解析这一行:{line}',
|
||||||
bodyMode: '请求体处理',
|
bodyMode: '请求体处理',
|
||||||
bodyModeOff: '默认',
|
bodyModeOff: '默认',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user