llmpt 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +130 -0
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +71 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/server/api/routes.d.ts +6 -0
  9. package/dist/server/api/routes.d.ts.map +1 -0
  10. package/dist/server/api/routes.js +128 -0
  11. package/dist/server/api/routes.js.map +1 -0
  12. package/dist/server/app.d.ts +14 -0
  13. package/dist/server/app.d.ts.map +1 -0
  14. package/dist/server/app.js +75 -0
  15. package/dist/server/app.js.map +1 -0
  16. package/dist/server/config/crypto.d.ts +14 -0
  17. package/dist/server/config/crypto.d.ts.map +1 -0
  18. package/dist/server/config/crypto.js +63 -0
  19. package/dist/server/config/crypto.js.map +1 -0
  20. package/dist/server/config/loader.d.ts +12 -0
  21. package/dist/server/config/loader.d.ts.map +1 -0
  22. package/dist/server/config/loader.js +142 -0
  23. package/dist/server/config/loader.js.map +1 -0
  24. package/dist/server/providers/anthropic.d.ts +9 -0
  25. package/dist/server/providers/anthropic.d.ts.map +1 -0
  26. package/dist/server/providers/anthropic.js +29 -0
  27. package/dist/server/providers/anthropic.js.map +1 -0
  28. package/dist/server/providers/base.d.ts +14 -0
  29. package/dist/server/providers/base.d.ts.map +1 -0
  30. package/dist/server/providers/base.js +15 -0
  31. package/dist/server/providers/base.js.map +1 -0
  32. package/dist/server/providers/ollama.d.ts +9 -0
  33. package/dist/server/providers/ollama.d.ts.map +1 -0
  34. package/dist/server/providers/ollama.js +24 -0
  35. package/dist/server/providers/ollama.js.map +1 -0
  36. package/dist/server/providers/openai.d.ts +9 -0
  37. package/dist/server/providers/openai.d.ts.map +1 -0
  38. package/dist/server/providers/openai.js +32 -0
  39. package/dist/server/providers/openai.js.map +1 -0
  40. package/dist/server/providers/registry.d.ts +23 -0
  41. package/dist/server/providers/registry.d.ts.map +1 -0
  42. package/dist/server/providers/registry.js +50 -0
  43. package/dist/server/providers/registry.js.map +1 -0
  44. package/dist/server/proxy/handler.d.ts +9 -0
  45. package/dist/server/proxy/handler.d.ts.map +1 -0
  46. package/dist/server/proxy/handler.js +101 -0
  47. package/dist/server/proxy/handler.js.map +1 -0
  48. package/dist/server/proxy/stream.d.ts +9 -0
  49. package/dist/server/proxy/stream.d.ts.map +1 -0
  50. package/dist/server/proxy/stream.js +200 -0
  51. package/dist/server/proxy/stream.js.map +1 -0
  52. package/dist/server/static.d.ts +6 -0
  53. package/dist/server/static.d.ts.map +1 -0
  54. package/dist/server/static.js +58 -0
  55. package/dist/server/static.js.map +1 -0
  56. package/dist/server/trace/collector.d.ts +59 -0
  57. package/dist/server/trace/collector.d.ts.map +1 -0
  58. package/dist/server/trace/collector.js +278 -0
  59. package/dist/server/trace/collector.js.map +1 -0
  60. package/dist/server/trace/sanitizer.d.ts +5 -0
  61. package/dist/server/trace/sanitizer.d.ts.map +1 -0
  62. package/dist/server/trace/sanitizer.js +45 -0
  63. package/dist/server/trace/sanitizer.js.map +1 -0
  64. package/dist/server/trace/writer.d.ts +26 -0
  65. package/dist/server/trace/writer.d.ts.map +1 -0
  66. package/dist/server/trace/writer.js +125 -0
  67. package/dist/server/trace/writer.js.map +1 -0
  68. package/dist/server/types.d.ts +167 -0
  69. package/dist/server/types.d.ts.map +1 -0
  70. package/dist/server/types.js +5 -0
  71. package/dist/server/types.js.map +1 -0
  72. package/dist/server/ws/server.d.ts +20 -0
  73. package/dist/server/ws/server.d.ts.map +1 -0
  74. package/dist/server/ws/server.js +78 -0
  75. package/dist/server/ws/server.js.map +1 -0
  76. package/dist/web/assets/index-BleB8LKa.js +21 -0
  77. package/dist/web/assets/index-DAsXJzMt.css +1 -0
  78. package/dist/web/assets/markdown-DsSlw-Z4.js +20 -0
  79. package/dist/web/assets/react-vendor-BnH7jZBV.js +21 -0
  80. package/dist/web/assets/rolldown-runtime-QTnfLwEv.js +1 -0
  81. package/dist/web/index.html +18 -0
  82. package/lpt.config.yaml +11 -0
  83. package/package.json +70 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## v1.1.0 (2026-06-15)
4
+
5
+ ### 新增
6
+
7
+ - **Thinking 内联展示**:响应区域的 Thinking 内容不再独立成一个顶层 Section,改为嵌在 Response Section 内部顶部,以折叠块形式展示(默认收起),与 messages 历史中的 ThinkingBlock 风格一致
8
+ - **Thinking Markdown 渲染**:Thinking 折叠块展开后支持「原文 / 渲染」切换,渲染模式走 react-markdown,修复了因父容器 `.content-block` 的 `white-space: pre-wrap` 继承导致空行堆积的问题
9
+ - **Thinking 复制按钮**:hover 到 Thinking 标题行时出现复制按钮,固定在行尾右侧
10
+ - **自定义 ConfirmDialog**:替换浏览器原生 `window.confirm`,所有删除确认操作改为居中精美弹窗,支持 Esc 取消、Enter 确认、毛玻璃遮罩、弹出动画
11
+
12
+ ### 优化
13
+
14
+ - Thinking 标题行三角符号与文字间距加宽
15
+ - Thinking 展开时 toggle 按钮(原文/渲染)与标题同行,不再另起一行
16
+ - 去掉 Thinking 标题行的行数统计(`N 行`)信息,界面更简洁
17
+
18
+ ---
19
+
20
+ ## v1.0.0 (2026-06-14)
21
+
22
+ 初始发布。
23
+
24
+ - 单进程单端口代理服务(默认 19900)
25
+ - 支持 OpenAI / Anthropic / Ollama 三类 Provider
26
+ - 实时 WebSocket 推送,Trace 列表自动更新
27
+ - Trace 详情:System Prompt、Messages、Response、Tool Calls、Raw JSON
28
+ - Markdown 渲染(react-markdown + highlight.js 代码高亮)
29
+ - Token 用量栏(prompt / completion / reasoning / cache)
30
+ - 深色 / 浅色主题切换
31
+ - Trace JSONL 持久化,重启可恢复历史
32
+ - AES-256-GCM API Key 加密存储
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zhxsking
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # LPT — LLM Proxy Trace
2
+
3
+ > 本地 LLM 代理工具,零侵入地拦截并可视化所有 OpenAI / Anthropic / Ollama API 调用
4
+
5
+ ```
6
+ 你的应用 ──► http://localhost:19900 ──► 真实 LLM Provider
7
+
8
+
9
+ Web Dashboard(实时 Trace 监控)
10
+ ```
11
+
12
+ ---
13
+
14
+ ## 功能特性
15
+
16
+ - **零侵入代理** — 只需把 `base_url` 改为 `http://localhost:19900`,无需修改任何应用代码
17
+ - **实时流式追踪** — WebSocket 推送,实时响应
18
+ - **完整 Trace 记录** — 请求体、响应体、思考链(thinking)、Tool Call、Token 用量全记录
19
+ - **Markdown 渲染** — 响应内容支持原文 / Markdown 双视图,代码块语法高亮
20
+ - **Token 用量统计** — 输入/输出/推理/缓存命中 Token 分项展示,实时速度统计
21
+
22
+ ---
23
+
24
+ ## 快速开始
25
+
26
+ ### 方式一:npx 一键启动(推荐)
27
+
28
+ 无需安装,直接运行:
29
+
30
+ ```bash
31
+ npx llmpt
32
+ ```
33
+
34
+ 首次运行会在当前目录生成 `lpt.config.yaml` 配置文件,按提示填入 API Key 后即可使用。
35
+
36
+ ### 方式二:全局安装
37
+
38
+ ```bash
39
+ npm install -g llmpt
40
+ llmpt
41
+ ```
42
+
43
+ ### 方式三:从源码运行
44
+
45
+ ```bash
46
+ git clone https://github.com/zhxsking/llm_proxy_trace.git
47
+ cd llm_proxy_trace
48
+ npm install
49
+ npm run build
50
+ npm start
51
+ ```
52
+
53
+ 启动后浏览器打开 `http://localhost:19900`。
54
+
55
+ ---
56
+
57
+ ## 配置
58
+
59
+ 启动后编辑当前目录的 `lpt.config.yaml`:
60
+
61
+ ```yaml
62
+ proxy:
63
+ port: 19900 # 代理端口
64
+
65
+ providers:
66
+ openai:
67
+ apiKey: "sk-..." # OpenAI API Key
68
+ baseUrl: "https://api.openai.com"
69
+
70
+ anthropic:
71
+ apiKey: "sk-ant-..." # Anthropic API Key
72
+
73
+ ollama:
74
+ baseUrl: "http://localhost:11434"
75
+ ```
76
+
77
+ 修改配置后重启服务生效。
78
+
79
+ ---
80
+
81
+ ## 接入客户端
82
+
83
+ 将 LLM SDK 的 `base_url` 指向本地代理,其余配置保持不变:
84
+
85
+ **Python(OpenAI SDK)**
86
+ ```python
87
+ from openai import OpenAI
88
+ client = OpenAI(
89
+ base_url="http://localhost:19900/v1",
90
+ api_key="any", # 代理会替换为配置文件中的真实 key
91
+ )
92
+ ```
93
+
94
+ **环境变量(适用于大多数客户端)**
95
+ ```bash
96
+ OPENAI_BASE_URL=http://localhost:19900/v1
97
+ OPENAI_API_KEY=any
98
+
99
+ # Anthropic
100
+ ANTHROPIC_BASE_URL=http://localhost:19900
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 技术栈
106
+
107
+ | 层 | 技术 |
108
+ |----|------|
109
+ | 运行时 | Node.js 18+ |
110
+ | 语言 | TypeScript 5.x |
111
+ | 后端框架 | [Hono](https://hono.dev) |
112
+ | 前端框架 | React 18 + Vite 6 |
113
+ | 样式 | CSS Variables + Tailwind CSS |
114
+ | 字体 | 系统默认字体栈 |
115
+ | 实时通信 | WebSocket (`ws`) |
116
+ | Markdown | react-markdown + remark-gfm + highlight.js |
117
+ | 配置 | js-yaml |
118
+ | 加密 | Node.js crypto(AES-256-GCM) |
119
+
120
+ ---
121
+
122
+ ## 致谢
123
+
124
+ 界面设计和 Trace 展示风格参考了 [claude-tap](https://github.com/liaohch3/claude-tap) 项目。
125
+
126
+ ---
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // ============================================================
3
+ // LPT CLI Entry Point - Unified server (proxy + dashboard + WS)
4
+ // ============================================================
5
+ import { createApp } from './server/app.js';
6
+ import { loadConfig } from './server/config/loader.js';
7
+ import path from 'node:path';
8
+ async function main() {
9
+ console.log('🚀 LPT (LLM Proxy Trace) 启动中...');
10
+ // Load configuration — pass explicit path so saveConfig writes back to the same file
11
+ const configPath = path.resolve(process.cwd(), 'lpt.config.yaml');
12
+ const config = loadConfig(configPath);
13
+ console.log(`📋 配置加载成功:端口 ${config.proxy.port}`);
14
+ // Create application
15
+ const { app, registry, collector, ws } = createApp(config, configPath);
16
+ // Create HTTP server with Hono handler
17
+ const { serve } = await import('@hono/node-server');
18
+ const server = serve({
19
+ fetch: app.fetch,
20
+ port: config.proxy.port,
21
+ });
22
+ // Attach WebSocket to the same HTTP server
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ ws.attachToServer(server, '/ws');
25
+ console.log(`\n🌐 服务运行中:http://localhost:${config.proxy.port}`);
26
+ console.log(`🔌 WebSocket:ws://localhost:${config.proxy.port}/ws`);
27
+ console.log(`📁 Trace 目录:${config.trace.dir}`);
28
+ console.log('\n 代理路由:');
29
+ console.log(' POST /v1/chat/completions (OpenAI)');
30
+ console.log(' POST /v1/completions (OpenAI)');
31
+ console.log(' GET /v1/models');
32
+ console.log(' POST /v1/messages (Anthropic)');
33
+ console.log(' POST /api/chat (Ollama)');
34
+ console.log(' 管理 API:');
35
+ console.log(' GET /api/traces');
36
+ console.log(' GET /api/traces/:id');
37
+ console.log(' GET /api/providers');
38
+ console.log(' PUT /api/providers/:type');
39
+ console.log(' GET /api/stats');
40
+ console.log('\n ⌨️ Ctrl+C 停止\n');
41
+ // Auto-open dashboard
42
+ if (config.dashboard.autoOpen) {
43
+ const url = `http://localhost:${config.proxy.port}`;
44
+ setTimeout(async () => {
45
+ try {
46
+ const { exec } = await import('node:child_process');
47
+ const cmd = process.platform === 'win32' ? `start ${url}` :
48
+ process.platform === 'darwin' ? `open ${url}` :
49
+ `xdg-open ${url}`;
50
+ exec(cmd);
51
+ }
52
+ catch { /* ignore */ }
53
+ }, 1500);
54
+ }
55
+ // Graceful shutdown
56
+ const shutdown = () => {
57
+ console.log('\n🛑 正在关闭服务...');
58
+ registry.stopRefreshTimers();
59
+ ws.stop();
60
+ server.close();
61
+ console.log('👋 已退出');
62
+ process.exit(0);
63
+ };
64
+ process.on('SIGINT', shutdown);
65
+ process.on('SIGTERM', shutdown);
66
+ }
67
+ main().catch((err) => {
68
+ console.error('启动失败:', err);
69
+ process.exit(1);
70
+ });
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { Hono } from 'hono';
2
+ import type { TraceCollector } from '../trace/collector.js';
3
+ import type { ProviderRegistry } from '../providers/registry.js';
4
+ import type { AppConfig } from '../types.js';
5
+ export declare function createApiRoutes(collector: TraceCollector, registry: ProviderRegistry, config: AppConfig, configPath?: string): Hono;
6
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/server/api/routes.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAgC,MAAM,aAAa,CAAC;AAI3E,wBAAgB,eAAe,CAC7B,SAAS,EAAE,cAAc,EACzB,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,SAAS,EACjB,UAAU,CAAC,EAAE,MAAM,GAClB,IAAI,CA0IN"}
@@ -0,0 +1,128 @@
1
+ // ============================================================
2
+ // Management API Routes
3
+ // ============================================================
4
+ import { Hono } from 'hono';
5
+ import path from 'node:path';
6
+ import { maskApiKey } from '../config/crypto.js';
7
+ import { saveConfig } from '../config/loader.js';
8
+ export function createApiRoutes(collector, registry, config, configPath) {
9
+ const api = new Hono();
10
+ // --- Trace APIs ---
11
+ api.get('/traces', (c) => {
12
+ const model = c.req.query('model');
13
+ const provider = c.req.query('provider');
14
+ const status = c.req.query('status');
15
+ const keyword = c.req.query('keyword');
16
+ const limit = parseInt(c.req.query('limit') || '50');
17
+ const offset = parseInt(c.req.query('offset') || '0');
18
+ const result = collector.search({ model, provider, status, keyword, limit, offset });
19
+ return c.json({
20
+ traces: result.traces,
21
+ total: result.total,
22
+ page: Math.floor(offset / limit) + 1,
23
+ pageSize: limit,
24
+ });
25
+ });
26
+ // POST /api/traces/reload → 静态路由必须在 /:id 动态路由之前
27
+ api.post('/traces/reload', (c) => {
28
+ const count = collector.loadFromDisk();
29
+ return c.json({ ok: true, loaded: count });
30
+ });
31
+ // GET /api/traces/files → 获取所有 JSONL 文件名列表
32
+ api.get('/traces/files', (c) => {
33
+ const files = collector.getWriter().getTraceFiles().map(f => path.basename(f));
34
+ return c.json({ files });
35
+ });
36
+ // DELETE /api/traces/file/:name → 删除指定文件及其内存记录
37
+ api.delete('/traces/file/:name', (c) => {
38
+ const name = c.req.param('name');
39
+ const ok = collector.deleteFile(name);
40
+ return c.json({ ok });
41
+ });
42
+ api.get('/traces/:id', (c) => {
43
+ const id = c.req.param('id');
44
+ const trace = collector.get(id);
45
+ if (!trace)
46
+ return c.json({ error: 'Trace not found' }, 404);
47
+ return c.json(trace);
48
+ });
49
+ api.delete('/traces', (c) => {
50
+ const permanent = c.req.query('files') === 'true';
51
+ collector.clear();
52
+ if (permanent) {
53
+ collector.getWriter().deleteAllFiles();
54
+ }
55
+ return c.json({ ok: true, permanent });
56
+ });
57
+ // --- Provider APIs ---
58
+ // GET /api/providers → 返回当前已配置的 provider 列表
59
+ api.get('/providers', (c) => {
60
+ const providers = registry.getAll().map((p) => ({
61
+ type: p.type,
62
+ baseUrl: p.config.baseUrl,
63
+ apiKey: p.config.apiKey ? maskApiKey(p.config.apiKey) : undefined,
64
+ }));
65
+ return c.json(providers);
66
+ });
67
+ // PUT /api/providers/:type → 更新某个 type 的配置
68
+ api.put('/providers/:type', async (c) => {
69
+ const type = c.req.param('type');
70
+ if (!['openai', 'anthropic', 'ollama'].includes(type)) {
71
+ return c.json({ error: 'Invalid provider type' }, 400);
72
+ }
73
+ const body = await c.req.json();
74
+ const existing = config.providers[type] || {};
75
+ const updated = {
76
+ baseUrl: body.baseUrl ?? existing.baseUrl ?? '',
77
+ apiKey: body.apiKey ?? existing.apiKey,
78
+ headers: body.headers ?? existing.headers,
79
+ };
80
+ config.providers[type] = updated;
81
+ registry.registerAll(config.providers);
82
+ saveConfig(config, configPath);
83
+ return c.json({ ok: true });
84
+ });
85
+ // DELETE /api/providers/:type → 移除某个 type
86
+ api.delete('/providers/:type', (c) => {
87
+ const type = c.req.param('type');
88
+ delete config.providers[type];
89
+ registry.registerAll(config.providers);
90
+ saveConfig(config, configPath);
91
+ return c.json({ ok: true });
92
+ });
93
+ // --- Config APIs ---
94
+ api.get('/config', (c) => {
95
+ return c.json({
96
+ proxy: config.proxy,
97
+ trace: { dir: config.trace.dir, maxFileSize: config.trace.maxFileSize },
98
+ dashboard: config.dashboard,
99
+ providerCount: Object.keys(config.providers).length,
100
+ });
101
+ });
102
+ // --- Stats APIs ---
103
+ api.get('/stats', (c) => {
104
+ const allTraces = collector.getAll();
105
+ const completed = allTraces.filter((t) => t.status === 'completed');
106
+ const totalCalls = allTraces.length;
107
+ const totalTokens = completed.reduce((sum, t) => sum + (t.usage?.totalTokens || 0), 0);
108
+ const avgDuration = completed.length > 0
109
+ ? completed.reduce((sum, t) => sum + t.duration, 0) / completed.length
110
+ : 0;
111
+ const callsByModel = {};
112
+ const callsByProvider = {};
113
+ for (const t of allTraces) {
114
+ callsByModel[t.model] = (callsByModel[t.model] || 0) + 1;
115
+ callsByProvider[t.provider] = (callsByProvider[t.provider] || 0) + 1;
116
+ }
117
+ return c.json({
118
+ totalCalls,
119
+ totalTokens,
120
+ avgDuration: Math.round(avgDuration),
121
+ callsByModel,
122
+ callsByProvider,
123
+ recentCalls: allTraces.slice(0, 10),
124
+ });
125
+ });
126
+ return api;
127
+ }
128
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../../src/server/api/routes.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,wBAAwB;AACxB,+DAA+D;AAE/D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,IAAI,MAAM,WAAW,CAAC;AAI7B,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD,MAAM,UAAU,eAAe,CAC7B,SAAyB,EACzB,QAA0B,EAC1B,MAAiB,EACjB,UAAmB;IAEnB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,qBAAqB;IAErB,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAgE,CAAC;QACpG,MAAM,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACvC,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC;QAEtD,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QACrF,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC;YACpC,QAAQ,EAAE,KAAK;SAChB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,gDAAgD;IAChD,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,EAAE,EAAE;QAC/B,MAAM,KAAK,GAAG,SAAS,CAAC,YAAY,EAAE,CAAC;QACvC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,2CAA2C;IAC3C,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC,CAAC,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC,aAAa,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/E,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,+CAA+C;IAC/C,GAAG,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC,CAAC,EAAE,EAAE;QACrC,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,EAAE,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACtC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE;QAC3B,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,EAAE,GAAG,CAAC,CAAC;QAC7D,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;QAC1B,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC;QAClD,SAAS,CAAC,KAAK,EAAE,CAAC;QAClB,IAAI,SAAS,EAAE,CAAC;YACd,SAAS,CAAC,SAAS,EAAE,CAAC,cAAc,EAAE,CAAC;QACzC,CAAC;QACD,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,wBAAwB;IAExB,4CAA4C;IAC5C,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE;QAC1B,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC9C,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO;YACzB,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS;SAClE,CAAC,CAAC,CAAC;QACJ,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,2CAA2C;IAC3C,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACtC,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAiB,CAAC;QACjD,IAAI,CAAC,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACtD,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,EAAE,GAAG,CAAC,CAAC;QACzD,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAA2B,CAAC;QACzD,MAAM,QAAQ,GAA4B,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACvE,MAAM,OAAO,GAAmB;YAC9B,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO,IAAI,EAAE;YAC/C,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM;YACtC,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO;SAC1C,CAAC;QACF,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;QACjC,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACvC,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAC/B,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,0CAA0C;IAC1C,GAAG,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC,CAAC,EAAE,EAAE;QACnC,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAiB,CAAC;QACjD,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACvC,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAC/B,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,sBAAsB;IAEtB,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;QACvB,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE;YACvE,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM;SACpD,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,qBAAqB;IAErB,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;QACtB,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC;QAEpE,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC;QACpC,MAAM,WAAW,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,WAAW,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACvF,MAAM,WAAW,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC;YACtC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,MAAM;YACtE,CAAC,CAAC,CAAC,CAAC;QAEN,MAAM,YAAY,GAA2B,EAAE,CAAC;QAChD,MAAM,eAAe,GAA2B,EAAE,CAAC;QACnD,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;YAC1B,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YACzD,eAAe,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QACvE,CAAC;QAED,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,UAAU;YACV,WAAW;YACX,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;YACpC,YAAY;YACZ,eAAe;YACf,WAAW,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;SACpC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { Hono } from 'hono';
2
+ import type { AppConfig } from './types.js';
3
+ import { ProviderRegistry } from './providers/registry.js';
4
+ import { TraceCollector } from './trace/collector.js';
5
+ import { WSServer } from './ws/server.js';
6
+ export interface AppContext {
7
+ app: Hono;
8
+ registry: ProviderRegistry;
9
+ collector: TraceCollector;
10
+ ws: WSServer;
11
+ config: AppConfig;
12
+ }
13
+ export declare function createApp(config: AppConfig, configPath?: string): AppContext;
14
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAK1C,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,IAAI,CAAC;IACV,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,EAAE,cAAc,CAAC;IAC1B,EAAE,EAAE,QAAQ,CAAC;IACb,MAAM,EAAE,SAAS,CAAC;CACnB;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,UAAU,CA2E5E"}
@@ -0,0 +1,75 @@
1
+ // ============================================================
2
+ // Hono App - 主应用入口
3
+ // ============================================================
4
+ import { Hono } from 'hono';
5
+ import { cors } from 'hono/cors';
6
+ import path from 'node:path';
7
+ import fs from 'node:fs';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { ProviderRegistry } from './providers/registry.js';
10
+ import { TraceCollector } from './trace/collector.js';
11
+ import { WSServer } from './ws/server.js';
12
+ import { handleProxy } from './proxy/handler.js';
13
+ import { createApiRoutes } from './api/routes.js';
14
+ import { serveStaticFiles } from './static.js';
15
+ export function createApp(config, configPath) {
16
+ const app = new Hono();
17
+ // Initialize services
18
+ const registry = new ProviderRegistry();
19
+ const collector = new TraceCollector(config.trace.dir, config.trace.maxFileSize, config.trace.maxInMemory);
20
+ const ws = new WSServer();
21
+ // Register providers from config
22
+ registry.registerAll(config.providers);
23
+ // Wire up WebSocket callbacks for trace events
24
+ collector.setCallbacks((trace) => ws.broadcast({ type: 'trace:new', data: trace }), (update) => ws.broadcast({ type: 'trace:update', data: update }));
25
+ // Middleware
26
+ // Security: restrict CORS to localhost origins only.
27
+ // This prevents any web page from a remote origin from reading traces or
28
+ // calling management APIs (which could expose LLM conversation contents).
29
+ app.use('*', cors({
30
+ origin: (origin) => {
31
+ if (!origin)
32
+ return origin; // same-origin / non-browser requests
33
+ try {
34
+ const url = new URL(origin);
35
+ const host = url.hostname;
36
+ if (host === 'localhost' || host === '127.0.0.1' || host === '::1')
37
+ return origin;
38
+ }
39
+ catch { /* ignore malformed origin */ }
40
+ return null; // reject everything else
41
+ },
42
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
43
+ allowHeaders: ['Content-Type', 'Authorization', 'x-api-key'],
44
+ }));
45
+ // --- Management API (MUST be before proxy catch-all) ---
46
+ const apiRoutes = createApiRoutes(collector, registry, config, configPath);
47
+ app.route('/api', apiRoutes);
48
+ // --- Health check ---
49
+ app.get('/health', (c) => c.json({ status: 'ok', proxy: true }));
50
+ // --- Proxy Routes ---
51
+ // Models endpoint(返回空列表,不再远程拉取)
52
+ app.get('/v1/models', (c) => c.json({ object: 'list', data: [] }));
53
+ app.get('/models', (c) => c.json({ object: 'list', data: [] }));
54
+ app.get('/api/tags', (c) => c.json({ models: [] }));
55
+ // OpenAI-compatible completions
56
+ app.post('/v1/chat/completions', async (c) => handleProxy(c, registry, collector, ws));
57
+ app.post('/chat/completions', async (c) => handleProxy(c, registry, collector, ws));
58
+ app.post('/v1/completions', async (c) => handleProxy(c, registry, collector, ws));
59
+ app.post('/completions', async (c) => handleProxy(c, registry, collector, ws));
60
+ // Anthropic-compatible
61
+ app.post('/v1/messages', async (c) => handleProxy(c, registry, collector, ws));
62
+ // Ollama-compatible
63
+ app.post('/api/chat', async (c) => handleProxy(c, registry, collector, ws));
64
+ app.post('/api/generate', async (c) => handleProxy(c, registry, collector, ws));
65
+ // Catch-all for any remaining /v1/* paths (except /api/* management routes)
66
+ app.all('/v1/*', async (c) => handleProxy(c, registry, collector, ws));
67
+ // --- Static file serving for frontend (SPA) ---
68
+ // Check for dist/web directory (built frontend)
69
+ const webDist = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../dist/web');
70
+ if (fs.existsSync(webDist)) {
71
+ app.use('*', serveStaticFiles(webDist));
72
+ }
73
+ return { app, registry, collector, ws, config };
74
+ }
75
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Encrypt a plaintext string → base64 encoded ciphertext
3
+ * Format: base64(iv[16] + authTag[16] + ciphertext)
4
+ */
5
+ export declare function encrypt(plaintext: string): string;
6
+ /**
7
+ * Decrypt a base64 encoded ciphertext → plaintext string
8
+ */
9
+ export declare function decrypt(ciphertext: string): string;
10
+ /**
11
+ * Mask an API key for display: sk-1234...abcd
12
+ */
13
+ export declare function maskApiKey(key: string): string;
14
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/server/config/crypto.ts"],"names":[],"mappings":"AA2BA;;;GAGG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAYjD;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAgBlD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG9C"}
@@ -0,0 +1,63 @@
1
+ // ============================================================
2
+ // API Key Encryption / Decryption (AES-256-GCM)
3
+ // ============================================================
4
+ import crypto from 'node:crypto';
5
+ const ALGORITHM = 'aes-256-gcm';
6
+ const IV_LENGTH = 16;
7
+ const TAG_LENGTH = 16;
8
+ // Fixed salt — unique to this application, provides domain separation.
9
+ // Key also incorporates machine identity so ciphertext is not portable
10
+ // between machines (reduces risk if config file is leaked).
11
+ const APP_SALT = 'lpt-proxy-trace-v1';
12
+ /**
13
+ * Derive a 32-byte key using HKDF from machine identity.
14
+ * Even if hostname/username both fall back to defaults, the fixed
15
+ * APP_SALT ensures the key is domain-separated from other apps.
16
+ */
17
+ function deriveKey() {
18
+ const hostname = process.env.COMPUTERNAME || process.env.HOSTNAME || 'localhost';
19
+ const username = process.env.USERNAME || process.env.USER || 'default';
20
+ const ikm = Buffer.from(`${hostname}:${username}`, 'utf-8');
21
+ // HKDF-SHA256: info = app salt, length = 32 bytes
22
+ return crypto.hkdfSync('sha256', ikm, Buffer.alloc(0), APP_SALT, 32);
23
+ }
24
+ /**
25
+ * Encrypt a plaintext string → base64 encoded ciphertext
26
+ * Format: base64(iv[16] + authTag[16] + ciphertext)
27
+ */
28
+ export function encrypt(plaintext) {
29
+ const key = deriveKey();
30
+ const iv = crypto.randomBytes(IV_LENGTH);
31
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
32
+ const encrypted = Buffer.concat([
33
+ cipher.update(plaintext, 'utf-8'),
34
+ cipher.final(),
35
+ ]);
36
+ const tag = cipher.getAuthTag();
37
+ return Buffer.concat([iv, tag, encrypted]).toString('base64');
38
+ }
39
+ /**
40
+ * Decrypt a base64 encoded ciphertext → plaintext string
41
+ */
42
+ export function decrypt(ciphertext) {
43
+ const key = deriveKey();
44
+ const combined = Buffer.from(ciphertext, 'base64');
45
+ if (combined.length < IV_LENGTH + TAG_LENGTH) {
46
+ throw new Error('Invalid ciphertext: too short');
47
+ }
48
+ const iv = combined.subarray(0, IV_LENGTH);
49
+ const tag = combined.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
50
+ const encrypted = combined.subarray(IV_LENGTH + TAG_LENGTH);
51
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
52
+ decipher.setAuthTag(tag);
53
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf-8');
54
+ }
55
+ /**
56
+ * Mask an API key for display: sk-1234...abcd
57
+ */
58
+ export function maskApiKey(key) {
59
+ if (!key || key.length <= 8)
60
+ return '***';
61
+ return `${key.slice(0, 4)}...${key.slice(-4)}`;
62
+ }
63
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../../src/server/config/crypto.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,gDAAgD;AAChD,+DAA+D;AAE/D,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,MAAM,SAAS,GAAG,aAAa,CAAC;AAChC,MAAM,SAAS,GAAG,EAAE,CAAC;AACrB,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,uEAAuE;AACvE,uEAAuE;AACvE,4DAA4D;AAC5D,MAAM,QAAQ,GAAG,oBAAoB,CAAC;AAEtC;;;;GAIG;AACH,SAAS,SAAS;IAChB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,WAAW,CAAC;IACjF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,SAAS,CAAC;IACvE,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,QAAQ,IAAI,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC;IAC5D,kDAAkD;IAClD,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAsB,CAAC;AAC5F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,SAAiB;IACvC,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC;IACxB,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAEzD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC;QAC9B,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC;QACjC,MAAM,CAAC,KAAK,EAAE;KACf,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAEhC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAChE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,OAAO,CAAC,UAAkB;IACxC,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC;IACxB,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAEnD,IAAI,QAAQ,CAAC,MAAM,GAAG,SAAS,GAAG,UAAU,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,EAAE,GAAU,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAClD,MAAM,GAAG,GAAS,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,UAAU,CAAC,CAAC;IACvE,MAAM,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,SAAS,GAAG,UAAU,CAAC,CAAC;IAE5D,MAAM,QAAQ,GAAG,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAC7D,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IAEzB,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AACzF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACjD,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { AppConfig } from '../types.js';
2
+ declare const DEFAULT_CONFIG: AppConfig;
3
+ /**
4
+ * Load config from YAML file, falling back to defaults
5
+ */
6
+ export declare function loadConfig(configPath?: string): AppConfig;
7
+ /**
8
+ * Save config back to YAML file (encrypts API keys)
9
+ */
10
+ export declare function saveConfig(config: AppConfig, configPath?: string): void;
11
+ export { DEFAULT_CONFIG };
12
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/server/config/loader.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAmC,MAAM,aAAa,CAAC;AA4C9E,QAAA,MAAM,cAAc,EAAE,SAerB,CAAC;AAEF;;GAEG;AACH,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAkCzD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAevE;AAmBD,OAAO,EAAE,cAAc,EAAE,CAAC"}