diff --git a/src/main.rs b/src/main.rs index dc31981..a9084a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,9 @@ mod kb; mod llm; mod exec; pub mod state; +mod template; mod timer; +mod tools; mod ws; use std::sync::Arc; @@ -19,6 +21,7 @@ pub struct AppState { pub config: Config, pub agent_mgr: Arc, pub kb: Option>, + pub obj_root: String, } #[derive(Debug, Clone, serde::Deserialize)] @@ -83,17 +86,28 @@ async fn main() -> anyhow::Result<()> { // Resume incomplete workflows after restart resume_workflows(database.pool.clone(), agent_mgr.clone()).await; + let obj_root = std::env::var("OBJ_ROOT").unwrap_or_else(|_| "/data/obj".to_string()); + let state = Arc::new(AppState { db: database, config: config.clone(), agent_mgr: agent_mgr.clone(), kb: kb_arc, + obj_root: obj_root.clone(), }); let app = Router::new() - .nest("/api", api::router(state)) - .nest("/ws", ws::router(agent_mgr)) - .fallback_service(ServeDir::new("web/dist").fallback(ServeFile::new("web/dist/index.html"))) + .nest("/tori/api", api::router(state)) + .nest("/api/obj", api::obj::router(obj_root.clone())) + .route("/api/obj/", axum::routing::get({ + let r = obj_root; + move || api::obj::root_listing(r) + })) + .nest("/ws/tori", ws::router(agent_mgr)) + .nest_service("/tori", ServeDir::new("web/dist").fallback(ServeFile::new("web/dist/index.html"))) + .route("/", axum::routing::get(|| async { + axum::response::Redirect::permanent("/tori/") + })) .layer(CorsLayer::permissive()); let addr = format!("{}:{}", &config.server.host, config.server.port); diff --git a/web/package-lock.json b/web/package-lock.json index 26e813a..28e9d9e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1127,6 +1127,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1324,6 +1325,7 @@ "version": "11.1.2", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz", "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", @@ -1374,6 +1376,7 @@ "version": "3.33.1", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "peer": true, "engines": { "node": ">=0.10" } @@ -1740,6 +1743,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true, "engines": { "node": ">=12" } @@ -2136,6 +2140,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -2314,6 +2319,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2350,6 +2356,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -2466,6 +2473,7 @@ "version": "3.5.29", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/compiler-sfc": "3.5.29", diff --git a/web/src/api.ts b/web/src/api.ts index 02c620b..2d5227d 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -1,6 +1,6 @@ import type { Project, Workflow, ExecutionLogEntry, Comment, Timer, KbArticle, KbArticleSummary, PlanStepInfo, LlmCallLogEntry } from './types' -const BASE = '/api' +const BASE = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/api` async function request(path: string, options?: RequestInit): Promise { const res = await fetch(`${BASE}${path}`, { @@ -102,4 +102,12 @@ export const api = { deleteArticle: (id: string) => request(`/kb/articles/${id}`, { method: 'DELETE' }), + + getSettings: () => request>('/settings'), + + putSetting: (key: string, value: string) => + request>(`/settings/${key}`, { + method: 'PUT', + body: JSON.stringify({ value }), + }), } diff --git a/web/src/ws.ts b/web/src/ws.ts index 2195686..e597e8d 100644 --- a/web/src/ws.ts +++ b/web/src/ws.ts @@ -51,7 +51,8 @@ export type WsHandler = (msg: WsMessage) => void export function connectWs(projectId: string, onMessage: WsHandler): { close: () => void } { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:' - const url = `${proto}//${location.host}/ws/${projectId}` + const wsBase = import.meta.env.BASE_URL.replace(/\/$/, '') + const url = `${proto}//${location.host}/ws${wsBase}/${projectId}` let ws: WebSocket | null = null let reconnectTimer: ReturnType | null = null let closed = false diff --git a/web/vite.config.ts b/web/vite.config.ts index e53fd2e..e9c83ee 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -2,11 +2,13 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ + base: process.env.VITE_BASE_PATH || '/tori/', plugins: [vue()], server: { proxy: { - '/api': 'http://localhost:3000', - '/ws': { + '/tori/api': 'http://localhost:3000', + '/api/obj': 'http://localhost:3000', + '/ws/tori': { target: 'ws://localhost:3000', ws: true, },