0.5 缩减重构

This commit is contained in:
lix-2026
2026-04-13 19:21:42 +08:00
parent af92c4b149
commit 71fb1aee7e
2023 changed files with 21113 additions and 394493 deletions
@@ -9,10 +9,12 @@ import { clamp } from "@/lib/constants";
import { useAppPreferencesStore } from "@/store/app-preferences";
type AgentMessage = { role: "user" | "assistant"; content: string };
type AiProvider = "online" | "local" | "ollama" | "codex";
type ToolLog =
| { type: "tool_call"; id: string; tool: string; args: Record<string, unknown> }
| { type: "tool_result"; id: string; tool: string; ok: boolean; ms: number; result: unknown }
| { type: "info"; message: string }
| { type: "error"; message: string };
type PanelPage = "chat" | "tools" | "settings";
@@ -37,6 +39,8 @@ const ONLINE_MODELS = [
"gemini-3-flash-preview",
] as const;
const OLLAMA_QWEN3_30B = "qwen3:30b-a3b-instruct-2507-q4_K_M";
const isRecord = (v: unknown): v is Record<string, unknown> =>
typeof v === "object" && v !== null && !Array.isArray(v);
@@ -95,10 +99,13 @@ export function OnlyOfficeAiAgentPanel({
const [networkOn, setNetworkOn] = useState(() => !flightMode);
const [toolAuto, setToolAuto] = useState(true);
const [aiProvider, setAiProvider] = useState<"online" | "local">("online");
const [aiProvider, setAiProvider] = useState<AiProvider>("online");
const [aiModel, setAiModel] = useState<string>("");
const [maxSteps, setMaxSteps] = useState<number>(10);
// Codex:每个面板对话维护一个 session,支持连续对话与“暂停(类似 ESC)”
const [codexSessionId, setCodexSessionId] = useState<string | null>(null);
const [pluginReady, setPluginReady] = useState(false);
const abortRef = useRef<AbortController | null>(null);
@@ -131,7 +138,7 @@ export function OnlyOfficeAiAgentPanel({
const providerRaw = (window.localStorage.getItem("onlyoffice_ai_provider") || "").trim();
const modelRaw = window.localStorage.getItem("onlyoffice_ai_model") || "";
const parsed = Number(stepsRaw);
if (providerRaw === "online" || providerRaw === "local") setAiProvider(providerRaw);
if (providerRaw === "online" || providerRaw === "local" || providerRaw === "ollama" || providerRaw === "codex") setAiProvider(providerRaw);
if (typeof modelRaw === "string") setAiModel(modelRaw);
if (Number.isFinite(parsed) && parsed >= 1) setMaxSteps(clamp(Math.floor(parsed), 1, 24));
} catch {
@@ -281,6 +288,9 @@ export function OnlyOfficeAiAgentPanel({
abortRef.current?.abort();
abortRef.current = null;
setLoading(false);
if (aiProvider === "codex") {
setToolLogs((prev) => [...prev, { type: "info", message: "已暂停(可继续对话)" }]);
}
};
const send = async () => {
@@ -288,6 +298,9 @@ export function OnlyOfficeAiAgentPanel({
if (!text) return;
if (loading) return;
const codexSessionIdForRequest =
aiProvider === "codex" ? String(codexSessionId ?? "").trim() || null : null;
const nextMessages: AgentMessage[] = [...messages, { role: "user", content: text }];
setMessages(nextMessages);
setInput("");
@@ -318,7 +331,14 @@ export function OnlyOfficeAiAgentPanel({
"toolset.onlyoffice_editor",
],
},
options: { searxng: networkOn, ai: { provider: aiProvider, model: aiModel } },
options: {
searxng: networkOn,
ai: {
provider: aiProvider,
...(aiProvider === "codex" && codexSessionIdForRequest ? { sessionId: codexSessionIdForRequest } : {}),
...(aiProvider !== "codex" && String(aiModel || "").trim() ? { model: String(aiModel || "").trim() } : {}),
},
},
}),
});
if (!res.ok) {
@@ -329,6 +349,19 @@ export function OnlyOfficeAiAgentPanel({
}
await parseSseChunks(res, (event, dataText) => {
if (event === "codex_session") {
try {
const d = JSON.parse(dataText || "null") as unknown;
const obj = isRecord(d) ? d : ({} as Record<string, unknown>);
const sid = String(obj.sessionId ?? "").trim();
if (sid) {
setCodexSessionId(sid);
}
} catch {
// ignore
}
return;
}
if (event === "assistant_message") {
try {
const d = JSON.parse(dataText || "null") as unknown;
@@ -386,6 +419,7 @@ export function OnlyOfficeAiAgentPanel({
}
if (event === "error") {
if (controller.signal.aborted && aiProvider === "codex") return;
try {
const d = JSON.parse(dataText || "null") as unknown;
const msg = isRecord(d) && "message" in d ? String(d.message ?? "") : "";
@@ -397,6 +431,7 @@ export function OnlyOfficeAiAgentPanel({
}
});
} catch (e) {
if (controller.signal.aborted && aiProvider === "codex") return;
const msg = e instanceof Error ? e.message : String(e);
setToolLogs((prev) => [...prev, { type: "error", message: msg }]);
} finally {
@@ -495,6 +530,13 @@ export function OnlyOfficeAiAgentPanel({
</div>
);
}
if (l.type === "info") {
return (
<div key={idx} className="rounded border bg-muted p-2 text-muted-foreground">
{l.message}
</div>
);
}
if (l.type === "tool_call") {
return (
<div key={idx} className="rounded border p-2">
@@ -544,29 +586,65 @@ export function OnlyOfficeAiAgentPanel({
<select
className="rounded border px-2 py-1 text-xs"
value={aiProvider}
onChange={(e) => setAiProvider(e.target.value === "local" ? "local" : "online")}
onChange={(e) => {
const v = String(e.target.value || "").trim();
if (v === "online" || v === "local" || v === "ollama" || v === "codex") setAiProvider(v);
else setAiProvider("online");
}}
disabled={loading}
>
<option value="online">线</option>
<option value="local"></option>
<option value="ollama">Ollama</option>
<option value="codex">Codex</option>
</select>
</label>
<label className="flex items-center justify-between gap-2">
<span className="font-medium"></span>
<select
className="w-[220px] rounded border px-2 py-1 text-xs"
value={aiModel}
onChange={(e) => setAiModel(e.target.value)}
disabled={loading || aiProvider !== "online"}
>
{ONLINE_MODELS.map((m) => (
<option key={m} value={m}>
{m || "默认"}
</option>
))}
</select>
{aiProvider === "codex" ? (
<div className="text-xs text-muted-foreground">
<code className="rounded bg-muted px-1 py-0.5">#chat</code> /{" "}
<code className="rounded bg-muted px-1 py-0.5">#test</code> /{" "}
<code className="rounded bg-muted px-1 py-0.5">#dev</code> <code className="rounded bg-muted px-1 py-0.5">#chat</code>
</div>
) : aiProvider === "online" ? (
<select
className="w-[220px] rounded border px-2 py-1 text-xs"
value={ONLINE_MODELS.includes(aiModel as any) ? aiModel : ""}
onChange={(e) => setAiModel(e.target.value)}
disabled={loading}
>
{ONLINE_MODELS.map((m) => (
<option key={m} value={m}>
{m || "默认"}
</option>
))}
</select>
) : aiProvider === "ollama" ? (
<select
className="w-[320px] rounded border px-2 py-1 text-xs"
value={aiModel === "" || aiModel === OLLAMA_QWEN3_30B ? aiModel : ""}
onChange={(e) => setAiModel(e.target.value)}
disabled={loading}
>
<option value="">{OLLAMA_QWEN3_30B}</option>
<option value={OLLAMA_QWEN3_30B}>{OLLAMA_QWEN3_30B}</option>
</select>
) : (
<input
className="w-[320px] rounded border px-2 py-1 text-xs"
value={aiModel}
onChange={(e) => setAiModel(e.target.value)}
placeholder="默认(ai.local.md/环境变量)"
disabled={loading}
list="oo-local-model-suggestions"
/>
)}
</label>
<datalist id="oo-local-model-suggestions">
<option value={OLLAMA_QWEN3_30B} />
</datalist>
</div>
) : null}
@@ -602,7 +680,7 @@ export function OnlyOfficeAiAgentPanel({
</Button>
<Button variant="secondary" disabled={!loading} onClick={stop}>
{aiProvider === "codex" ? "暂停" : "停止"}
</Button>
</div>
</div>