Archived
feat(api): 实装 /api/status 接口——无鉴权公开接口,换算比率权威来源
- endpoints:AuthMode 扩展 'none'(完全公开),追加 /api/status 定义(考证实证文案 + 实测示例响应),置于列表首位 - endpoint-info-line:鉴权徽标三态化(公开/宽松/严格,map 驱动 + none 中性样式) - endpoint-panel:requiresKey——无鉴权接口隐藏 sk-key 输入行、按钮不因空 key 禁用、空态文案区分 - spec:接口数 5→6、顺序、鉴权分布(none+strict×3+lenient×2)断言更新 - docs:接口文档同步 479 行新版(/api/status 详解 + QuotaPerUnit 考证);交互设计 §1 清单加第 0 行;README 与 side-nav 计数更新 - 验证:test 64/64 + type-check + lint + build 四绿
This commit is contained in:
@@ -3,17 +3,18 @@ import { describe, expect, it } from 'vitest';
|
||||
import { endpointList, findEndpointByPath } from './endpoints';
|
||||
|
||||
describe('endpointList 数据完整性', () => {
|
||||
it('恰好 5 个接口(设计文档 §1 定案的对接清单)', () => {
|
||||
expect(endpointList).toHaveLength(5);
|
||||
it('恰好 6 个接口(设计文档 §1 定案的对接清单)', () => {
|
||||
expect(endpointList).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('路径唯一(path 是 tab / queryKey 的唯一标识)', () => {
|
||||
const pathList = endpointList.map((endpoint) => endpoint.path);
|
||||
expect(new Set(pathList).size).toBe(5);
|
||||
expect(new Set(pathList).size).toBe(6);
|
||||
});
|
||||
|
||||
it('顺序与用户指定清单一致', () => {
|
||||
expect(endpointList.map((endpoint) => endpoint.path)).toEqual([
|
||||
'/api/status',
|
||||
'/v1/dashboard/billing/subscription',
|
||||
'/v1/dashboard/billing/usage',
|
||||
'/v1/models',
|
||||
@@ -25,13 +26,14 @@ describe('endpointList 数据完整性', () => {
|
||||
it('全部为 GET 且带鉴权元数据', () => {
|
||||
for (const endpoint of endpointList) {
|
||||
expect(endpoint.method).toBe('GET');
|
||||
expect(['strict', 'lenient']).toContain(endpoint.authMode);
|
||||
expect(['strict', 'lenient', 'none']).toContain(endpoint.authMode);
|
||||
expect(endpoint.authMiddlewareName).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('严格/宽松鉴权分布正确(前 3 严格,后 2 宽松)', () => {
|
||||
it('鉴权模式分布正确(公开 1 + 严格 3 + 宽松 2)', () => {
|
||||
expect(endpointList.map((endpoint) => endpoint.authMode)).toEqual([
|
||||
'none',
|
||||
'strict',
|
||||
'strict',
|
||||
'strict',
|
||||
|
||||
+32
-3
@@ -1,17 +1,17 @@
|
||||
/**
|
||||
* 5 个对接接口的静态元数据
|
||||
* 对接接口的静态元数据
|
||||
* 数据源:docs/NewAPI-sk-key接口文档.md(源码实证调研)
|
||||
* 顺序即 sider / tab 展示顺序(设计文档 §1 定案,不分组不编号)
|
||||
*/
|
||||
|
||||
export type AuthMode = 'strict' | 'lenient';
|
||||
export type AuthMode = 'strict' | 'lenient' | 'none';
|
||||
|
||||
export interface EndpointDefinition {
|
||||
/** 接口路径(唯一标识,同时用作 tab 标签与 queryKey 组成部分) */
|
||||
readonly path: string;
|
||||
/** HTTP 方法(当前全部为 GET,保留扩展位) */
|
||||
readonly method: 'GET';
|
||||
/** 鉴权模式:strict = TokenAuth(key 无效/过期/耗尽直接被拒);lenient = TokenAuthReadOnly(宽松,废 key 也能查) */
|
||||
/** 鉴权模式:strict = TokenAuth(key 无效/过期/耗尽直接被拒);lenient = TokenAuthReadOnly(宽松,废 key 也能查);none = 完全公开(无需任何凭证) */
|
||||
readonly authMode: AuthMode;
|
||||
/** 鉴权中间件名(展示用) */
|
||||
readonly authMiddlewareName: string;
|
||||
@@ -24,6 +24,35 @@ export interface EndpointDefinition {
|
||||
}
|
||||
|
||||
export const endpointList: readonly EndpointDefinition[] = [
|
||||
{
|
||||
path: '/api/status',
|
||||
method: 'GET',
|
||||
authMode: 'none',
|
||||
authMiddlewareName: '无(公开接口)',
|
||||
responseSummary: '站点配置与计费参数(含 quota_per_unit)',
|
||||
notes: [
|
||||
'完全公开:无任何鉴权,不持有 sk-key 也能访问(本工具对接清单中唯一)',
|
||||
'quota_per_unit 是 QuotaPerUnit 当前生效值(默认 500000,源码 $0.002 / 1K tokens 锚点)——quota ÷ 该值 = 美元,换算比率的唯一权威来源,管理员可改,不要硬编码',
|
||||
'接口①④交叉实测吻合验证不了管理员是否改过此值:billing 接口内部就是拿 quota ÷ QuotaPerUnit 换算,同一常量自证',
|
||||
'quota_display_type 影响 billing 接口语义:USD 直接是美元;CNY 先 ÷QuotaPerUnit 再 ×usd_exchange_rate;TOKENS 是原始 token 数',
|
||||
'返回 60+ 字段(版本、登录方式、前端模块开关、公告等),解析时只取计费相关字段,其余忽略;字段集合随版本演进,需容忍未知字段',
|
||||
],
|
||||
sampleResponse: {
|
||||
success: true,
|
||||
message: '',
|
||||
data: {
|
||||
quota_per_unit: 500000,
|
||||
quota_display_type: 'USD',
|
||||
usd_exchange_rate: 6.8,
|
||||
display_in_currency: true,
|
||||
custom_currency_symbol: '¤',
|
||||
custom_currency_exchange_rate: 1,
|
||||
price: 6.8,
|
||||
system_name: 'AI Gateway',
|
||||
version: 'v1.0.0-rc.35',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/v1/dashboard/billing/subscription',
|
||||
method: 'GET',
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 接口信息行:method 徽标 + 路径 + 鉴权徽标(宽松/严格)
|
||||
* 接口信息行:method 徽标 + 路径 + 鉴权徽标(公开/宽松/严格)
|
||||
*/
|
||||
|
||||
import type { EndpointDefinition } from '@/api/endpoints';
|
||||
|
||||
const props = defineProps<{ endpoint: EndpointDefinition }>();
|
||||
|
||||
const authBadgeText = props.endpoint.authMode === 'lenient' ? '宽松' : '严格';
|
||||
const authBadgeTitle = `${props.endpoint.authMiddlewareName}(${
|
||||
props.endpoint.authMode === 'lenient'
|
||||
? 'key 过期/禁用/额度耗尽仍可查询'
|
||||
: 'key 无效/过期/额度耗尽会被拒绝'
|
||||
})`;
|
||||
const authBadgeTextMap: Record<string, string> = {
|
||||
lenient: '宽松',
|
||||
strict: '严格',
|
||||
none: '公开',
|
||||
};
|
||||
|
||||
const authBadgeTitleMap: Record<string, string> = {
|
||||
lenient: 'TokenAuthReadOnly(key 过期/禁用/额度耗尽仍可查询)',
|
||||
strict: 'TokenAuth(key 无效/过期/额度耗尽会被拒绝)',
|
||||
none: '无鉴权中间件(不持有任何 key 也能访问)',
|
||||
};
|
||||
|
||||
const authBadgeText = authBadgeTextMap[props.endpoint.authMode] ?? '未知';
|
||||
const authBadgeTitle =
|
||||
props.endpoint.authMode === 'none'
|
||||
? authBadgeTitleMap.none
|
||||
: `${props.endpoint.authMiddlewareName}(${
|
||||
props.endpoint.authMode === 'lenient'
|
||||
? 'key 过期/禁用/额度耗尽仍可查询'
|
||||
: 'key 无效/过期/额度耗尽会被拒绝'
|
||||
})`;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -64,6 +79,11 @@ const authBadgeTitle = `${props.endpoint.authMiddlewareName}(${
|
||||
border: 1px solid var(--td-success-color);
|
||||
}
|
||||
|
||||
.auth-badge.none {
|
||||
color: var(--td-text-color-secondary);
|
||||
border: 1px solid var(--td-component-border);
|
||||
}
|
||||
|
||||
.response-summary {
|
||||
font-size: 12px;
|
||||
color: var(--td-text-color-placeholder);
|
||||
|
||||
@@ -44,12 +44,17 @@ const {
|
||||
sendRequest,
|
||||
} = useManualEndpointQuery(endpointPathRef, skKeyInput);
|
||||
|
||||
/** 无鉴权接口(如 /api/status)不需要 sk-key:隐藏输入行、按钮不因空 key 禁用 */
|
||||
const requiresKey = computed(() => endpoint.value.authMode !== 'none');
|
||||
|
||||
const keyInputStatus = computed(() =>
|
||||
hasSent.value && !skKeyInput.value.trim() ? 'error' : 'default',
|
||||
hasSent.value && requiresKey.value && !skKeyInput.value.trim() ? 'error' : 'default',
|
||||
);
|
||||
|
||||
const sendButtonDisabled = computed(() => requiresKey.value && !skKeyInput.value.trim());
|
||||
|
||||
function handleSend(): void {
|
||||
if (!skKeyInput.value.trim()) return; // 空 key 不发(输入框已标红提示)
|
||||
if (sendButtonDisabled.value) return; // 空 key 不发(输入框已标红提示);无鉴权接口直接发
|
||||
sendRequest();
|
||||
}
|
||||
|
||||
@@ -100,10 +105,11 @@ function downloadResponseAsFile(): void {
|
||||
</t-collapse>
|
||||
</section>
|
||||
|
||||
<!-- 调试行:sk-key + 发送 -->
|
||||
<!-- 调试行:sk-key + 发送(无鉴权接口隐藏 key 输入) -->
|
||||
<section class="panel-section">
|
||||
<div class="request-row">
|
||||
<t-input
|
||||
v-if="requiresKey"
|
||||
v-model="skKeyInput"
|
||||
class="sk-key-input"
|
||||
type="password"
|
||||
@@ -113,10 +119,11 @@ function downloadResponseAsFile(): void {
|
||||
:tips="keyInputStatus === 'error' ? '请填写 sk-key' : ''"
|
||||
@enter="handleSend"
|
||||
/>
|
||||
<span v-else class="no-key-hint">公开接口,无需 sk-key</span>
|
||||
<t-button
|
||||
theme="primary"
|
||||
:loading="isFetching"
|
||||
:disabled="!skKeyInput.trim()"
|
||||
:disabled="sendButtonDisabled"
|
||||
@click="handleSend"
|
||||
>
|
||||
发送请求
|
||||
@@ -156,7 +163,7 @@ function downloadResponseAsFile(): void {
|
||||
<JsonTextView :json="responseData.json" :raw-text="responseData.text" />
|
||||
</section>
|
||||
<section v-else-if="!hasSent" class="panel-section response-placeholder">
|
||||
填写 sk-key 后点击「发送请求」
|
||||
{{ requiresKey ? '填写 sk-key 后点击「发送请求」' : '点击「发送请求」查看站点配置' }}
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -186,6 +193,12 @@ function downloadResponseAsFile(): void {
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.no-key-hint {
|
||||
align-self: center;
|
||||
font-size: 12.5px;
|
||||
color: var(--td-text-color-placeholder);
|
||||
}
|
||||
|
||||
.response-section {
|
||||
flex: 1;
|
||||
min-height: 220px; /* 响应区保底高度:窄窗口下不被上部固定分区压缩到不可用 */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 左侧接口导航:5 个路径平铺(不分组不编号,按 endpoints.ts 定案顺序)
|
||||
* 左侧接口导航:路径平铺(不分组不编号,按 endpoints.ts 定案顺序)
|
||||
* 点击 → openOrActivateTab(已开则激活,未开则新开)
|
||||
*/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user