File size: 5,352 Bytes
c7d34c1
11486ce
b1cfe1b
 
c7d34c1
 
11486ce
 
 
c7d34c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e8ea5d
 
c7d34c1
11486ce
 
 
 
96bdf6c
 
a2c232b
11486ce
 
 
 
 
 
 
 
c7d34c1
 
11486ce
 
 
 
 
a2c232b
b1cfe1b
 
11486ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { buildAgentCapabilities } from '@/lib/agent-api-contracts';
import { RequestValidationError } from '@/lib/image-request-utils';
import { normalizeConfiguredUpstreamHeaders, type UpstreamRequestHeaders } from '@/lib/image-upstream-profile';
import { parseImageProviderManifest } from '@/lib/image-upstream-provider-manifest';
import { NextResponse } from 'next/server';

const CONFIGURED_MARKER = 'configured';
const REDACTED_CHANNEL_FIELDS = new Set(['API_KEYS', 'MATSCA_APP_ID', 'MATSCA_APP_SECRET', 'USER_AGENT']);

export async function GET() {
    // 保持未鉴权,便于 Agent 在发起工具调用前发现契约和鉴权要求。
    return NextResponse.json(buildAgentCapabilities(readPublicCapabilitiesEnv()));
}

function readPublicCapabilitiesEnv(): Record<string, string | undefined> {
    return {
        AGENT_STATE_BACKEND: process.env.AGENT_STATE_BACKEND,
        AGENT_API_TOKEN: process.env.AGENT_API_TOKEN?.trim() ? 'configured' : undefined,
        APP_PASSWORD: process.env.APP_PASSWORD?.trim() ? 'configured' : undefined,
        AGENT_DATABASE_URL: process.env.AGENT_DATABASE_URL ? 'configured' : undefined,
        AGENT_DB_PASSWORD: process.env.AGENT_DB_PASSWORD ? 'configured' : undefined,
        AGENT_DB_PASSWORD_FILE: process.env.AGENT_DB_PASSWORD_FILE ? 'configured' : undefined,
        AGENT_REQUEST_TTL_SECONDS: process.env.AGENT_REQUEST_TTL_SECONDS,
        AGENT_PUBLIC_BASE_URL: process.env.AGENT_PUBLIC_BASE_URL,
        ENABLE_RESPONSES_IMAGE_BACKEND: process.env.ENABLE_RESPONSES_IMAGE_BACKEND,
        OPENAI_RESPONSES_API_MODEL: process.env.OPENAI_RESPONSES_API_MODEL?.trim() ? 'configured' : undefined,
        NEXT_PUBLIC_IMAGE_STORAGE_MODE: process.env.NEXT_PUBLIC_IMAGE_STORAGE_MODE,
        VERCEL: process.env.VERCEL,
        OPENAI_API_KEY: process.env.OPENAI_API_KEY?.trim() ? 'configured' : undefined,
        OPENAI_API_BASE_URL: process.env.OPENAI_API_BASE_URL,
        OPENAI_UPSTREAM_PROFILE: process.env.OPENAI_UPSTREAM_PROFILE,
        OPENAI_UPSTREAM_REQUEST_MODES: process.env.OPENAI_UPSTREAM_REQUEST_MODES,
        OPENAI_UPSTREAM_REQUEST_MODE_PRIORITY: process.env.OPENAI_UPSTREAM_REQUEST_MODE_PRIORITY,
        OPENAI_UPSTREAM_PROXY_URL: process.env.OPENAI_UPSTREAM_PROXY_URL,
        OPENAI_UPSTREAM_USER_AGENT: readConfiguredMarker(process.env.OPENAI_UPSTREAM_USER_AGENT),
        UPSTREAM_USER_AGENT: readConfiguredMarker(process.env.UPSTREAM_USER_AGENT),
        OPENAI_ROUTING_STRATEGY: process.env.OPENAI_ROUTING_STRATEGY,
        OPENAI_ALLOWED_PLAIN_HTTP_API_BASE_URLS: process.env.OPENAI_ALLOWED_PLAIN_HTTP_API_BASE_URLS,
        IMAGE_UPSTREAM_TIMEOUT_MS: process.env.IMAGE_UPSTREAM_TIMEOUT_MS,
        IMAGE_STREAM_DATA_INTERVAL_TIMEOUT_MS: process.env.IMAGE_STREAM_DATA_INTERVAL_TIMEOUT_MS,
        IMAGE_UPSTREAM_MAX_RETRIES: process.env.IMAGE_UPSTREAM_MAX_RETRIES,
        ...readPublicChannelEnv(process.env)
    };
}

function readPublicChannelEnv(env: NodeJS.ProcessEnv): Record<string, string | undefined> {
    const publicEnv: Record<string, string | undefined> = {};
    for (const key of Object.keys(env)) {
        const match =
            /^OPENAI_CHANNEL_(\d+)_(ID|BASE_URL|PROXY_URL|UPSTREAM_PROFILE|PROVIDER_MANIFEST|REQUEST_MODES|REQUEST_MODE_PRIORITY|API_KEYS|MATSCA_APP_ID|MATSCA_APP_SECRET|USER_AGENT|UPSTREAM_HEADERS_JSON)$/.exec(
                key
            );
        if (!match) continue;
        const [, , fieldName] = match;
        publicEnv[key] = readPublicChannelEnvValue(key, fieldName, env[key]);
    }
    return publicEnv;
}

function readPublicChannelEnvValue(key: string, fieldName: string, value: string | undefined): string | undefined {
    if (REDACTED_CHANNEL_FIELDS.has(fieldName)) return readConfiguredMarker(value);
    if (fieldName === 'UPSTREAM_HEADERS_JSON') return sanitizePublicUpstreamHeadersJson(key, value);
    if (fieldName === 'PROVIDER_MANIFEST') return sanitizePublicProviderManifest(value);
    return value;
}

function readConfiguredMarker(value: string | undefined): string | undefined {
    return value?.trim() ? CONFIGURED_MARKER : undefined;
}

function sanitizePublicUpstreamHeadersJson(key: string, value: string | undefined): string | undefined {
    if (!value?.trim()) return undefined;
    let parsed: unknown;
    try {
        parsed = JSON.parse(value);
    } catch {
        throw new RequestValidationError(`${key} 必须是 JSON 对象。`, 500);
    }
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        throw new RequestValidationError(`${key} 必须是 JSON 对象。`, 500);
    }
    const headers: UpstreamRequestHeaders = {};
    for (const [name, headerValue] of Object.entries(parsed)) {
        if (typeof headerValue !== 'string') {
            throw new RequestValidationError(`${key} 的值必须都是字符串。`, 500);
        }
        headers[name] = headerValue;
    }
    const normalized = normalizeConfiguredUpstreamHeaders(headers, key);
    const sanitized = Object.fromEntries(
        Object.keys(normalized || {}).map((name) => [name, CONFIGURED_MARKER])
    ) as UpstreamRequestHeaders;
    return JSON.stringify(sanitized);
}

function sanitizePublicProviderManifest(value: string | undefined): string | undefined {
    if (!value?.trim()) return undefined;
    return JSON.stringify(parseImageProviderManifest(value));
}