fix: 修复云端聊天鉴权及容器兼容并支持3200原地升级
This commit is contained in:
@@ -5,15 +5,37 @@ export const runtime = "nodejs";
|
||||
const allowed = new Set(["chat", "sessions", "files", "skills", "tokens", "config", "xingbang"]);
|
||||
|
||||
async function proxy(request:NextRequest, {params}:{params:{path:string[]}}) {
|
||||
// General chat has filesystem/tools, unlike the scoped project APIs: local only.
|
||||
if (!["localhost","127.0.0.1","::1","[::1]"].includes(request.nextUrl.hostname))
|
||||
return NextResponse.json({detail:"原始开发聊天仅允许本机访问"},{status:403});
|
||||
if (!allowed.has(params.path[0]) || params.path.some(p=>!p || p==="." || p===".." || /[\\/]/.test(p)))
|
||||
return NextResponse.json({detail:"接口不存在"},{status:404});
|
||||
const base = process.env.CORE_BACKEND_URL || process.env.RISK_AGENT_BACKEND_URL || "http://127.0.0.1:8002";
|
||||
try {
|
||||
// Production nextUrl can contain the bind address (0.0.0.0), not the client Host.
|
||||
// Never use Host as authentication: all production core/tool access requires admin.
|
||||
const authorization = request.headers.get("authorization") || "";
|
||||
const host = request.headers.get("host") || "";
|
||||
const requiresAuth = process.env.NODE_ENV === "production" || process.env.XINGBANG_REQUIRE_AUTH === "1" || !!process.env.XINGBANG_AUTH_USERS || !!authorization;
|
||||
if (requiresAuth) {
|
||||
if (!authorization.startsWith("Bearer ") || !authorization.slice(7).trim())
|
||||
return NextResponse.json({detail:"请先在风险研判页面连接管理员访问身份(401)"},{status:401});
|
||||
const identity = await fetch(`${base}/api/xingbang/risk-agent/access/context`, {
|
||||
headers:{Authorization:authorization,"X-Xingbang-Client-Host":host},
|
||||
cache:"no-store",redirect:"error",signal:request.signal,
|
||||
});
|
||||
if (!identity.ok) return NextResponse.json({detail:"访问身份校验失败"},{status:identity.status === 401 || identity.status === 403 ? identity.status : 503});
|
||||
const user = await identity.json();
|
||||
if (user.role !== "admin" || user.preview || !user.id)
|
||||
return NextResponse.json({detail:"通用聊天与文件工具仅限已认证管理员"},{status:403});
|
||||
} else {
|
||||
let hostname = "";
|
||||
try { hostname = new URL(`http://${host}`).hostname; } catch { /* reject malformed Host */ }
|
||||
if (!["localhost","127.0.0.1","::1","[::1]"].includes(hostname))
|
||||
return NextResponse.json({detail:"未配置身份认证,仅允许本机开发访问"},{status:403});
|
||||
const origin = request.headers.get("origin");
|
||||
if ((origin && new URL(origin).host !== host) || request.headers.get("sec-fetch-site") === "cross-site")
|
||||
return NextResponse.json({detail:"不允许跨站访问开发工具"},{status:403});
|
||||
}
|
||||
const upstream = await fetch(`${base}/api/${params.path.map(encodeURIComponent).join("/")}${request.nextUrl.search}`, {
|
||||
method:request.method,headers:{"Content-Type":request.headers.get("content-type")||"application/json"},
|
||||
method:request.method,headers:{"Content-Type":request.headers.get("content-type")||"application/json",Authorization:authorization},
|
||||
body:["GET","HEAD"].includes(request.method)?undefined:await request.arrayBuffer(),
|
||||
cache:"no-store",redirect:"error",signal:request.signal,
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {Activity, ArrowRight, CheckCircle2, Database, FlaskConical, Play, RefreshCw, ShieldCheck, SlidersHorizontal, Square, X} from "lucide-react";
|
||||
import AgentNav from "@/components/xingbang/AgentNav";
|
||||
import {newRequestId} from "@/lib/request-id";
|
||||
import "./operations.css";
|
||||
|
||||
const ROOT = "/api/risk-agent/project-assistant/operations";
|
||||
@@ -53,7 +54,7 @@ export default function OperationsCenter() {
|
||||
async function act(fn:()=>Promise<void>){setBusy(true);setError("");setNotice("");try{await fn();setOverview(await api(`/overview?domain=${domain}`));}catch(e:any){setError(e.message);}finally{setBusy(false);}}
|
||||
function edit(key:string,value:any){setForm((f:any)=>({...f,[key]:value}));setDirty(true);}
|
||||
function chooseView(next:string){setView(next);history.replaceState({},"",`?domain=${domain}&view=${next}`);}
|
||||
async function submit(){await act(async()=>{const run=await api("/lab/runs",{source_revision:source.project.revision,model,requirement:model?requirement:""},crypto.randomUUID());setSelected(run.id);setView("monitor");await loadSource();setNotice("真实任务已入队。执行记录会自动刷新;历史快照保持不变。");});}
|
||||
async function submit(){await act(async()=>{const run=await api("/lab/runs",{source_revision:source.project.revision,model,requirement:model?requirement:""},newRequestId());setSelected(run.id);setView("monitor");await loadSource();setNotice("真实任务已入队。执行记录会自动刷新;历史快照保持不变。");});}
|
||||
const runs=(overview?.runs||[]).filter((r:any)=>(status==="all"||r.status===status)&&(!search||`${r.project_id} ${r.id}`.toLowerCase().includes(search.toLowerCase())));
|
||||
const run=detail?.run;
|
||||
const oldChecks=new Map<string,any>((comparison?.run?.checks||[]).map((c:any)=>[c.code,c]));
|
||||
|
||||
@@ -4,6 +4,7 @@ import {ArrowRight, FileText, RefreshCw, ShieldCheck} from "lucide-react";
|
||||
import AgentNav from "@/components/xingbang/AgentNav";
|
||||
import AssistantConfiguration from "@/components/xingbang/AssistantConfiguration";
|
||||
import AssistantHistory from "@/components/xingbang/AssistantHistory";
|
||||
import {newRequestId} from "@/lib/request-id";
|
||||
import "../data-access/workspace.css";
|
||||
|
||||
const tabs = [["overview", "概览"], ["findings", "材料与问题"], ["scenario", "方案比较"], ["report", "报告底稿"], ["experience", "反馈与经验"]];
|
||||
@@ -22,7 +23,7 @@ export default function XingbangWorkspace() {
|
||||
const selected = useRef("");
|
||||
const headers = (json = true) => {const token = sessionStorage.getItem("xingbang-access-token"); return {...(json ? {"Content-Type": "application/json"} : {}), ...(token ? {Authorization: `Bearer ${token}`} : {})};};
|
||||
async function request(path: string, body?: any) {
|
||||
const response = await fetch(`/api/risk-agent/project-assistant${path}`, {method: body === undefined ? "GET" : "POST", headers: {...headers(), ...(/\/(runs|reports)$/.test(path) && body ? {"Idempotency-Key": crypto.randomUUID()} : {})}, body: body === undefined ? undefined : JSON.stringify(body)});
|
||||
const response = await fetch(`/api/risk-agent/project-assistant${path}`, {method: body === undefined ? "GET" : "POST", headers: {...headers(), ...(/\/(runs|reports)$/.test(path) && body ? {"Idempotency-Key": newRequestId()} : {})}, body: body === undefined ? undefined : JSON.stringify(body)});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw Error(typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail || "操作失败"));
|
||||
return data;
|
||||
|
||||
@@ -49,6 +49,7 @@ export default function ChatInput() {
|
||||
</div>
|
||||
<p className="text-center text-[10px] text-gray-400/70 mt-2">
|
||||
Powered by DeepSeek · mini OpenClaw v0.1
|
||||
{" · "}<a href="/data-access" className="underline">云端聊天:先连接管理员访问身份</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import {readChatStream} from "./chat-stream";
|
||||
import {coreFetch as fetch} from "./core-fetch";
|
||||
|
||||
// Same-origin streaming proxy shares the current workbench backend configuration.
|
||||
const API_BASE = "/api/core";
|
||||
@@ -21,7 +22,7 @@ export async function* streamChat(
|
||||
message: string,
|
||||
sessionId: string
|
||||
): AsyncGenerator<SSEEvent> {
|
||||
yield* readChatStream(`${API_BASE}/chat`, {message,session_id:sessionId,stream:true});
|
||||
yield* readChatStream(`${API_BASE}/chat`, {message,session_id:sessionId,stream:true}, {fetcher:fetch});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/** Reuse the existing access identity without putting credentials in URLs or logs. */
|
||||
export const coreFetch: typeof fetch = (input, init) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
const token = typeof window === "undefined" ? null : window.sessionStorage.getItem("xingbang-access-token");
|
||||
if (token) headers.set("Authorization", `Bearer ${token}`);
|
||||
return globalThis.fetch(input, {...init, headers});
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
/** getRandomValues also works on HTTP intranet pages where randomUUID is unavailable. */
|
||||
export function newRequestId(): string {
|
||||
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
||||
bytes[6] = (bytes[6] & 15) | 64;
|
||||
bytes[8] = (bytes[8] & 63) | 128;
|
||||
const hex = Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
|
||||
return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user