mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
feat: output channel redesign - per-channel output gates, LLM chain events, SDKConfig
- Output channels generate per-channel tools: output_send__{name} (type=output) + output_send__{name}_help
- content is JSON string transparently passed to plugin handler for routing
- EventAgentLLMChain: full LLM response forwarded after each turn for webui/logs
- sdk.New refactored to SDKConfig struct (no more 13 positional args)
- RegisterOutputChannel adds desc param for JSON format documentation
- channelDevice simplified (no Tools method), desc field added
- Child agent permission updated for output_send__ prefix
- System prompt: output gates, multi-call, long messages split
- WebUI: subscribes to EventAgentLLMChain in SSE, no output channel
- Tests updated for new naming convention
This commit is contained in:
4
go.mod
4
go.mod
@ -8,6 +8,8 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require github.com/yanyiwu/gojieba v1.4.7 // indirect
|
||||
|
||||
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0-20260708004841-e9bdcf9304b0 // direct
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => ../homeagentsdk
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/homeagent-sdk-repo
|
||||
|
||||
2
go.sum
2
go.sum
@ -14,6 +14,8 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/yanyiwu/gojieba v1.4.7 h1:2YkXELcYLTE0SJetq6xv4MjpEikWga6VpFn4jIFFQ/k=
|
||||
github.com/yanyiwu/gojieba v1.4.7/go.mod h1:JUq4DddFVGdHXJHxxepxRmhrKlDpaBxR8O28v6fKYLY=
|
||||
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
||||
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
|
||||
103
gui/main.js
Normal file
103
gui/main.js
Normal file
@ -0,0 +1,103 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const CONNECTIONS_FILE = path.join(app.getPath('userData'), 'connections.json');
|
||||
|
||||
function loadConnections() {
|
||||
try {
|
||||
if (fs.existsSync(CONNECTIONS_FILE)) {
|
||||
return JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf-8'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load connections:', e);
|
||||
}
|
||||
return { connections: [], currentId: null };
|
||||
}
|
||||
|
||||
function saveConnections(data) {
|
||||
try {
|
||||
fs.writeFileSync(CONNECTIONS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
||||
} catch (e) {
|
||||
console.error('Failed to save connections:', e);
|
||||
}
|
||||
}
|
||||
|
||||
let mainWindow;
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 860,
|
||||
minWidth: 900,
|
||||
minHeight: 600,
|
||||
title: 'HomeAgent',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
||||
|
||||
if (process.argv.includes('--dev')) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle('connections:list', () => {
|
||||
return loadConnections();
|
||||
});
|
||||
|
||||
ipcMain.handle('connections:add', (_, conn) => {
|
||||
const data = loadConnections();
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
||||
data.connections.push({ id, name: conn.name, url: conn.url, apiKey: conn.apiKey });
|
||||
if (!data.currentId) data.currentId = id;
|
||||
saveConnections(data);
|
||||
return data;
|
||||
});
|
||||
|
||||
ipcMain.handle('connections:update', (_, { id, updates }) => {
|
||||
const data = loadConnections();
|
||||
const idx = data.connections.findIndex(c => c.id === id);
|
||||
if (idx !== -1) {
|
||||
data.connections[idx] = { ...data.connections[idx], ...updates };
|
||||
saveConnections(data);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
ipcMain.handle('connections:delete', (_, id) => {
|
||||
const data = loadConnections();
|
||||
data.connections = data.connections.filter(c => c.id !== id);
|
||||
if (data.currentId === id) {
|
||||
data.currentId = data.connections.length > 0 ? data.connections[0].id : null;
|
||||
}
|
||||
saveConnections(data);
|
||||
return data;
|
||||
});
|
||||
|
||||
ipcMain.handle('connections:setCurrent', (_, id) => {
|
||||
const data = loadConnections();
|
||||
if (data.connections.some(c => c.id === id)) {
|
||||
data.currentId = id;
|
||||
saveConnections(data);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow === null) createWindow();
|
||||
});
|
||||
801
gui/package-lock.json
generated
Normal file
801
gui/package-lock.json
generated
Normal file
@ -0,0 +1,801 @@
|
||||
{
|
||||
"name": "homeagent-gui",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "homeagent-gui",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"electron": "^33.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
||||
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"env-paths": "^2.2.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"got": "^11.8.5",
|
||||
"progress": "^2.0.3",
|
||||
"semver": "^6.2.0",
|
||||
"sumchecker": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"global-agent": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defer-to-connect": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cacheable-request": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-cache-semantics": "*",
|
||||
"@types/keyv": "^3.1.4",
|
||||
"@types/node": "*",
|
||||
"@types/responselike": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/keyv": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
|
||||
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/responselike": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/boolean": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-lookup": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-request": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
|
||||
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clone-response": "^1.0.2",
|
||||
"get-stream": "^5.1.0",
|
||||
"http-cache-semantics": "^4.0.0",
|
||||
"keyv": "^4.0.0",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"normalize-url": "^6.0.1",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-response": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response/node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/defer-to-connect": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/define-properties": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.0",
|
||||
"object-keys": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "33.4.11",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz",
|
||||
"integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^20.9.0",
|
||||
"extract-zip": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"electron": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20.55"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"bin": {
|
||||
"extract-zip": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/yauzl": "^2.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"es6-error": "^4.1.1",
|
||||
"matcher": "^3.0.0",
|
||||
"roarr": "^2.15.3",
|
||||
"semver": "^7.3.2",
|
||||
"serialize-error": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/globalthis": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
|
||||
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"define-properties": "^1.2.1",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/got": {
|
||||
"version": "11.8.6",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
|
||||
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.0.0",
|
||||
"@szmarczak/http-timer": "^4.0.5",
|
||||
"@types/cacheable-request": "^6.0.1",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^5.0.3",
|
||||
"cacheable-request": "^7.0.2",
|
||||
"decompress-response": "^6.0.0",
|
||||
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-property-descriptors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/http2-wrapper": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/matcher": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-url": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-cancelable": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-alpn": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/responselike": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
|
||||
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lowercase-keys": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/roarr": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"detect-node": "^2.0.4",
|
||||
"globalthis": "^1.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"semver-compare": "^1.0.0",
|
||||
"sprintf-js": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/serialize-error": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"type-fest": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sumchecker": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
gui/package.json
Normal file
13
gui/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "homeagent-gui",
|
||||
"version": "1.0.0",
|
||||
"description": "HomeAgent Desktop GUI - Multi-connection management dashboard",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron . --no-sandbox",
|
||||
"dev": "electron . --no-sandbox --dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron": "^33.0.0"
|
||||
}
|
||||
}
|
||||
11
gui/preload.js
Normal file
11
gui/preload.js
Normal file
@ -0,0 +1,11 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('homeagent', {
|
||||
connections: {
|
||||
list: () => ipcRenderer.invoke('connections:list'),
|
||||
add: (conn) => ipcRenderer.invoke('connections:add', conn),
|
||||
update: (id, updates) => ipcRenderer.invoke('connections:update', { id, updates }),
|
||||
delete: (id) => ipcRenderer.invoke('connections:delete', id),
|
||||
setCurrent: (id) => ipcRenderer.invoke('connections:setCurrent', id),
|
||||
},
|
||||
});
|
||||
713
gui/renderer/app.js
Normal file
713
gui/renderer/app.js
Normal file
@ -0,0 +1,713 @@
|
||||
// ===== State =====
|
||||
let state = {
|
||||
connections: [], currentConn: null,
|
||||
status: {}, kernel: null,
|
||||
settings: {}, meta: {}, pluginMeta: {}, settingsPlugins: ['core'],
|
||||
selectedSection: 'core',
|
||||
messages: [], chatLoading: false, chatStage: '',
|
||||
installedPlugins: [], pluginInfo: null,
|
||||
startedAt: null, uptimeTick: null, sidebarRefreshTick: null, eventSource: null,
|
||||
_chatHistoryLoaded: false, _starmapData: null,
|
||||
};
|
||||
|
||||
// ===== Markdown Renderer (lightweight, no dependencies) =====
|
||||
function renderMarkdown(t) {
|
||||
if (!t) return '';
|
||||
let s = String(t)
|
||||
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
// code blocks (fenced)
|
||||
s = s.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
|
||||
// inline code
|
||||
s = s.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
// headers
|
||||
s = s.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||||
s = s.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
||||
s = s.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
||||
// bold & italic
|
||||
s = s.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
||||
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||
s = s.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||
// links
|
||||
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
// images
|
||||
s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%">');
|
||||
// blockquote
|
||||
s = s.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');
|
||||
// horizontal rule
|
||||
s = s.replace(/^---$/gm, '<hr>');
|
||||
// unordered list
|
||||
s = s.replace(/^[\s]*[-*] (.+)$/gm, '<li>$1</li>');
|
||||
s = s.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
|
||||
// ordered list
|
||||
s = s.replace(/^[\s]*\d+\. (.+)$/gm, '<li>$1</li>');
|
||||
// paragraphs: double newlines
|
||||
s = s.replace(/\n\n/g, '</p><p>');
|
||||
s = '<p>' + s + '</p>';
|
||||
// clean nested ps from lists
|
||||
s = s.replace(/<\/p>\n?<ul>/g, '<ul>').replace(/<\/ul>\n?<p>/g, '</ul>');
|
||||
s = s.replace(/<\/p>\n?<li>/g, '<li>').replace(/<\/li>\n?<p>/g, '</li>');
|
||||
s = s.replace(/<p><\/p>/g, '');
|
||||
return s;
|
||||
}
|
||||
|
||||
// ===== Connection Management =====
|
||||
async function initApp() {
|
||||
const data = await window.homeagent.connections.list();
|
||||
state.connections = data.connections || [];
|
||||
if (data.currentId) state.currentConn = state.connections.find(c => c.id === data.currentId) || null;
|
||||
if (state.currentConn) {
|
||||
document.getElementById('app').style.display = 'block';
|
||||
document.getElementById('conn-overlay').style.display = 'none';
|
||||
updateConnIndicator();
|
||||
await renderAll();
|
||||
startUptimeTicker(); startSidebarRefresh(); connectSSE();
|
||||
} else {
|
||||
document.getElementById('conn-overlay').style.display = 'flex';
|
||||
}
|
||||
renderConnList();
|
||||
}
|
||||
|
||||
function updateConnIndicator() {
|
||||
const el = document.getElementById('conn-name-display');
|
||||
const dot = document.getElementById('conn-dot');
|
||||
if (state.currentConn) {
|
||||
el.textContent = state.currentConn.name;
|
||||
dot.className = 'status-dot ' + (state.status.status === 'running' ? 'dot-green' : 'dot-yellow');
|
||||
} else { el.textContent = '未连接'; dot.className = 'status-dot dot-gray'; }
|
||||
}
|
||||
|
||||
function openConnManager() { renderConnList(); document.getElementById('conn-overlay').style.display = 'flex'; }
|
||||
|
||||
async function selectConnection(id) {
|
||||
disconnectSSE();
|
||||
const data = await window.homeagent.connections.setCurrent(id);
|
||||
state.currentConn = data.connections.find(c => c.id === id) || null;
|
||||
state.connections = data.connections;
|
||||
state.messages = []; state._chatHistoryLoaded = false;
|
||||
document.getElementById('app').style.display = 'block';
|
||||
document.getElementById('conn-overlay').style.display = 'none';
|
||||
updateConnIndicator();
|
||||
await renderAll();
|
||||
startUptimeTicker(); startSidebarRefresh(); connectSSE();
|
||||
}
|
||||
|
||||
async function deleteConnection(id, e) {
|
||||
e.stopPropagation();
|
||||
if (!confirm('确定删除此连接?')) return;
|
||||
const wasCurrent = state.currentConn && state.currentConn.id === id;
|
||||
const data = await window.homeagent.connections.delete(id);
|
||||
state.connections = data.connections;
|
||||
state.currentConn = data.currentId ? state.connections.find(c => c.id === data.currentId) : null;
|
||||
if (wasCurrent) { disconnectSSE(); if (state.sidebarRefreshTick) { clearInterval(state.sidebarRefreshTick); state.sidebarRefreshTick = null; } }
|
||||
if (state.currentConn) {
|
||||
updateConnIndicator(); await renderAll(); startSidebarRefresh(); connectSSE();
|
||||
} else {
|
||||
document.getElementById('app').style.display = 'none'; document.getElementById('conn-overlay').style.display = 'flex';
|
||||
}
|
||||
renderConnList();
|
||||
}
|
||||
|
||||
function renderConnList() {
|
||||
document.getElementById('conn-list').innerHTML = state.connections.map(c =>
|
||||
'<div class="conn-item ' + (state.currentConn && state.currentConn.id === c.id ? 'active' : '') + '" onclick="selectConnection(\'' + c.id + '\')">'
|
||||
+ '<span class="status-dot ' + (state.currentConn && state.currentConn.id === c.id ? 'dot-green' : 'dot-gray') + '"></span>'
|
||||
+ '<div class="conn-info"><div class="conn-name">' + escHtml(c.name) + '</div><div class="conn-url">' + escHtml(c.url) + '</div></div>'
|
||||
+ '<div class="conn-actions">'
|
||||
+ '<button class="btn btn-ghost btn-sm" onclick="editConnection(\'' + c.id + '\', event)">编辑</button>'
|
||||
+ '<button class="btn btn-danger btn-sm" onclick="deleteConnection(\'' + c.id + '\', event)">删除</button></div></div>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
let editingConnId = null;
|
||||
|
||||
function showConnForm() {
|
||||
editingConnId = null;
|
||||
document.getElementById('conn-form-title').textContent = '添加连接 / Add Connection';
|
||||
document.getElementById('conn-name').value = 'My HomeAgent';
|
||||
document.getElementById('conn-url').value = 'http://localhost:8080';
|
||||
document.getElementById('conn-key').value = '';
|
||||
document.getElementById('conn-form').style.display = 'block';
|
||||
document.getElementById('conn-add-btn').style.display = 'none';
|
||||
}
|
||||
|
||||
function editConnection(id, e) {
|
||||
e.stopPropagation();
|
||||
const c = state.connections.find(x => x.id === id);
|
||||
if (!c) return;
|
||||
editingConnId = id;
|
||||
document.getElementById('conn-form-title').textContent = '编辑连接 / Edit Connection';
|
||||
document.getElementById('conn-name').value = c.name;
|
||||
document.getElementById('conn-url').value = c.url;
|
||||
document.getElementById('conn-key').value = c.apiKey;
|
||||
document.getElementById('conn-form').style.display = 'block';
|
||||
document.getElementById('conn-add-btn').style.display = 'none';
|
||||
document.querySelectorAll('.conn-item').forEach(el => el.style.opacity = '0.4');
|
||||
}
|
||||
|
||||
function cancelConnForm() {
|
||||
document.getElementById('conn-form').style.display = 'none';
|
||||
document.getElementById('conn-add-btn').style.display = 'block';
|
||||
document.querySelectorAll('.conn-item').forEach(el => el.style.opacity = '1');
|
||||
}
|
||||
|
||||
async function saveConnForm() {
|
||||
const name = document.getElementById('conn-name').value.trim();
|
||||
const url = document.getElementById('conn-url').value.trim().replace(/\/+$/, '');
|
||||
const apiKey = document.getElementById('conn-key').value.trim();
|
||||
if (!name || !url) { toast('名称和地址不能为空', true); return; }
|
||||
// test connection before saving
|
||||
const testBtn = document.querySelector('#conn-form .btn-primary');
|
||||
testBtn.textContent = '测试中...'; testBtn.disabled = true;
|
||||
try {
|
||||
const testR = await fetch(url + '/api/v1/status', {
|
||||
headers: apiKey ? { 'X-API-Key': apiKey } : {}
|
||||
});
|
||||
if (!testR.ok) { toast('连接测试失败: HTTP ' + testR.status, true); testBtn.textContent = '保存 / Save'; testBtn.disabled = false; return; }
|
||||
} catch(e) {
|
||||
toast('无法连接到 ' + url + ': ' + e.message, true);
|
||||
testBtn.textContent = '保存 / Save'; testBtn.disabled = false; return;
|
||||
}
|
||||
testBtn.textContent = '保存 / Save'; testBtn.disabled = false;
|
||||
let data;
|
||||
if (editingConnId) {
|
||||
data = await window.homeagent.connections.update(editingConnId, { name, url, apiKey });
|
||||
} else {
|
||||
data = await window.homeagent.connections.add({ name, url, apiKey });
|
||||
}
|
||||
state.connections = data.connections;
|
||||
const curId = data.currentId;
|
||||
const cur = data.connections.find(c => c.id === curId);
|
||||
if (cur) {
|
||||
state.currentConn = cur;
|
||||
if (!document.getElementById('app').style.display || document.getElementById('app').style.display === 'none') {
|
||||
document.getElementById('app').style.display = 'block';
|
||||
document.getElementById('conn-overlay').style.display = 'none';
|
||||
updateConnIndicator(); await renderAll(); startUptimeTicker(); startSidebarRefresh(); connectSSE();
|
||||
} else { updateConnIndicator(); if (editingConnId) await renderAll(); }
|
||||
}
|
||||
cancelConnForm(); renderConnList();
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && document.getElementById('conn-form').style.display === 'block') cancelConnForm();
|
||||
});
|
||||
|
||||
// ===== API Client =====
|
||||
async function api(path, opts = {}) {
|
||||
if (!state.currentConn) throw new Error('No connection selected');
|
||||
const headers = { 'Content-Type': 'application/json', ...opts.headers };
|
||||
if (state.currentConn.apiKey) headers['X-API-Key'] = state.currentConn.apiKey;
|
||||
const url = state.currentConn.url + '/api/v1' + path;
|
||||
const res = await fetch(url, { ...opts, headers });
|
||||
if (res.status === 401) throw new Error('unauthorized');
|
||||
if (opts.raw) return res;
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
if (ct.includes('json')) return res.json();
|
||||
return res.text();
|
||||
}
|
||||
|
||||
// ===== Toast =====
|
||||
function toast(msg, isError) {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg; t.className = 'toast' + (isError ? ' error' : ''); t.style.display = 'block';
|
||||
setTimeout(function() { t.style.display = 'none' }, 3000);
|
||||
}
|
||||
|
||||
// ===== Utility =====
|
||||
function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
function fmtUptime(ms) {
|
||||
const s = Math.floor(ms / 1000);
|
||||
if (s < 60) return s + 's'; const m = Math.floor(s / 60); s = s % 60;
|
||||
if (m < 60) return m + 'm ' + s + 's'; const h = Math.floor(m / 60); m = m % 60;
|
||||
return h + 'h ' + m + 'm ' + s + 's';
|
||||
}
|
||||
|
||||
// ===== Theme =====
|
||||
function setTheme(name) {
|
||||
document.documentElement.setAttribute('data-theme', name);
|
||||
localStorage.setItem('ha-theme', name);
|
||||
document.getElementById('theme-btn').textContent = name === 'light' ? '☀️' : '🌙';
|
||||
}
|
||||
function toggleTheme() { setTheme(document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light'); }
|
||||
(function() { setTheme(localStorage.getItem('ha-theme') || 'dark') })();
|
||||
|
||||
// ===== Navigation =====
|
||||
function switchTab(n) {
|
||||
document.querySelectorAll('.tab-content').forEach(function(e) { e.classList.remove('active') });
|
||||
const el = document.getElementById('tab-' + n); if (el) el.classList.add('active');
|
||||
document.querySelectorAll('nav a').forEach(function(e) { e.classList.remove('active') });
|
||||
const m = document.querySelector('nav a[onclick*="' + n + '"]'); if (m) m.classList.add('active');
|
||||
renderAll();
|
||||
}
|
||||
|
||||
// ===== Tab Render Dispatch =====
|
||||
async function renderAll() {
|
||||
if (!state.currentConn) return;
|
||||
try { const s = await api('/status'); state.status = s; state.startedAt = s.startedAt ? new Date(s.startedAt).getTime() : null; updateConnIndicator() } catch(e) {}
|
||||
try { state.kernel = await api('/kernel') } catch(e) {}
|
||||
try {
|
||||
const s = await api('/settings'); state.settings = s.settings || {}; state.meta = s.meta || {};
|
||||
state.settingsPlugins = s.plugins || ['core']; state.pluginMeta = s.plugin_meta || {};
|
||||
} catch(e) {}
|
||||
try { state.installedPlugins = await api('/plugins') } catch(e) {}
|
||||
try { renderOverview() } catch(e) {} try { renderChat() } catch(e) {} try { renderPlugins() } catch(e) {}
|
||||
try { renderKernel() } catch(e) {} try { renderOneSettings() } catch(e) {} try { renderAdapters() } catch(e) {}
|
||||
}
|
||||
|
||||
function startUptimeTicker() {
|
||||
if (state.uptimeTick) clearInterval(state.uptimeTick);
|
||||
state.uptimeTick = setInterval(function() {
|
||||
const el = document.querySelector('#uptime-val');
|
||||
if (el && state.startedAt) el.textContent = fmtUptime(Date.now() - state.startedAt);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// ===== SSE =====
|
||||
function disconnectSSE() { if (state.eventSource) { state.eventSource.close(); state.eventSource = null; } }
|
||||
|
||||
function connectSSE() {
|
||||
disconnectSSE(); if (!state.currentConn) return;
|
||||
connectFetchSSE(state.currentConn.url + '/api/v1/chat/events');
|
||||
}
|
||||
|
||||
async function connectFetchSSE(url) {
|
||||
try {
|
||||
const headers = {};
|
||||
if (state.currentConn && state.currentConn.apiKey) headers['X-API-Key'] = state.currentConn.apiKey;
|
||||
const resp = await fetch(url, { headers, cache: 'no-store' });
|
||||
if (!resp.ok || !resp.body) { setTimeout(function() { connectSSE() }, 5000); return; }
|
||||
const reader = resp.body.getReader(); const decoder = new TextDecoder();
|
||||
let buffer = ''; let reconnectTimer = null;
|
||||
state.eventSource = { close: function() { reader.cancel(); if (reconnectTimer) clearTimeout(reconnectTimer) } };
|
||||
function processLines() {
|
||||
const lines = buffer.split('\n'); buffer = lines.pop() || '';
|
||||
let eventType = '', data = '';
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) eventType = line.slice(7).trim();
|
||||
else if (line.startsWith('data: ')) data = line.slice(6).trim();
|
||||
else if (line === '' && eventType && data) { handleSSEEvent(eventType, data); eventType = ''; data = ''; }
|
||||
}
|
||||
}
|
||||
function handleSSEEvent(type, raw) {
|
||||
try {
|
||||
const p = JSON.parse(raw);
|
||||
if (type === 'agent_output') {
|
||||
const last = state.messages[state.messages.length - 1];
|
||||
if (last && last.role === 'assistant' && last._streaming) {
|
||||
last.content = (last.content || '') + (p.content || '');
|
||||
rerenderChatIfActive();
|
||||
}
|
||||
} else if (type === 'stage') {
|
||||
const phase = p.payload?.phase;
|
||||
if (phase === 'thinking') state.chatStage = 'Thinking...';
|
||||
else if (phase === 'before_toolcall') state.chatStage = 'Tool: ' + (p.payload?.tool || '');
|
||||
else if (phase === 'before_output') state.chatStage = 'Output...';
|
||||
updateChatStageBadge();
|
||||
} else if (type === 'reasoning') {
|
||||
const last = state.messages[state.messages.length - 1];
|
||||
if (last && last.role === 'assistant' && last._streaming) {
|
||||
last.reasoning_content = (last.reasoning_content || '') + (p.payload?.content || '');
|
||||
}
|
||||
}
|
||||
} catch(err) {}
|
||||
}
|
||||
async function pump() {
|
||||
while (true) {
|
||||
try { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); processLines(); } catch(e) { break; }
|
||||
}
|
||||
reconnectTimer = setTimeout(function() { connectSSE() }, 3000);
|
||||
}
|
||||
pump();
|
||||
} catch(e) { setTimeout(function() { connectSSE() }, 5000); }
|
||||
}
|
||||
|
||||
function startSidebarRefresh() {
|
||||
if (state.sidebarRefreshTick) clearInterval(state.sidebarRefreshTick);
|
||||
state.sidebarRefreshTick = setInterval(async function() {
|
||||
try { await loadSidebarData() } catch(e) {}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// ===== Overview =====
|
||||
function statCard(l, v) { return '<div class="card stat-card"><div class="stat-value">' + v + '</div><div class="stat-label">' + l + '</div></div>'; }
|
||||
|
||||
function renderOverview() {
|
||||
const s = state.status || {}; const k = state.kernel;
|
||||
let html = '<div class="grid-4">' + statCard('Status', s.status || 'unknown')
|
||||
+ statCard('Uptime', '<span id="uptime-val">' + (state.startedAt ? fmtUptime(Date.now() - state.startedAt) : '-') + '</span>')
|
||||
+ statCard('Plugins', (k?.plugins || []).length || 0)
|
||||
+ statCard('Version', s.version || '-') + '</div>';
|
||||
if (k) {
|
||||
html += '<div class="grid-2">'
|
||||
+ '<div class="card"><h2>LLM Status</h2>'
|
||||
+ '<div class="kv-row"><span class="key">Provider</span><span class="val">' + (k.llm?.provider || 'Not configured') + '</span></div>'
|
||||
+ '<div class="kv-row"><span class="key">Sources</span><span class="val">' + (k.llm?.sources || 0) + '</span></div>'
|
||||
+ '<div class="kv-row"><span class="key">Status</span><span class="val"><span class="status-dot ' + (k.llm?.available ? 'dot-green' : 'dot-red') + '"></span>' + (k.llm?.available ? 'Running' : 'Unavailable') + '</span></div></div>'
|
||||
+ '<div class="card"><h2>Memory Status</h2>'
|
||||
+ '<div class="kv-row"><span class="key">Graph Memory</span><span class="val"><span class="status-dot ' + (k.memory?.available ? 'dot-green' : 'dot-gray') + '"></span>' + (k.memory?.available ? k.memory.entity_count + ' entities, ' + k.memory.relation_count + ' relations' : 'Uninitialized') + '</span></div>'
|
||||
+ '<div class="kv-row"><span class="key">Document Memory</span><span class="val">' + (k.documents?.available ? k.documents.doc_count + ' docs' : 'Uninitialized') + '</span></div>'
|
||||
+ '<div class="kv-row"><span class="key">Text Memory</span><span class="val">' + (k.text_memory?.available ? k.text_memory.file_count + ' files' : 'Uninitialized') + '</span></div>'
|
||||
+ '<div class="kv-row"><span class="key">Knowledge</span><span class="val">' + (k.knowledge?.available ? k.knowledge.item_count + ' items' : 'Uninitialized') + '</span></div></div></div>';
|
||||
}
|
||||
html += '<div class="card"><h2>Runtime</h2><div class="grid-3">' + statCard('Goroutines', k?.runtime?.goroutines || '-') + statCard('Memory', k?.runtime?.memory_mb ? k.runtime.memory_mb + ' MB' : '-') + statCard('Go Version', k?.runtime?.go_version || '-') + '</div></div>'
|
||||
+ '<div class="card"><h2>Memory Graph</h2><div id="starmap-container" style="height:280px;background:var(--bg-input);border-radius:8px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);font-size:13px">'
|
||||
+ '<span id="starmap-placeholder">Loading memory graph...</span></div></div>';
|
||||
document.getElementById('tab-overview').innerHTML = html;
|
||||
loadStarmapData();
|
||||
}
|
||||
|
||||
async function loadStarmapData() {
|
||||
try {
|
||||
const resp = await api('/memory/graph');
|
||||
if (resp && resp.success && resp.data && resp.data.nodes && resp.data.nodes.length > 0) {
|
||||
state._starmapData = resp.data;
|
||||
document.getElementById('starmap-placeholder').textContent = resp.data.nodes.length + ' nodes, ' + (resp.data.edges?.length || 0) + ' edges';
|
||||
} else {
|
||||
document.getElementById('starmap-placeholder').textContent = 'No memory data yet';
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('starmap-placeholder').textContent = 'Failed to load: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Chat =====
|
||||
let _chatLayoutBuilt = false;
|
||||
let _terminals = [], _cmdHistory = [];
|
||||
|
||||
function buildChatLayout() {
|
||||
const k = state.kernel || {};
|
||||
document.getElementById('tab-chat').innerHTML =
|
||||
'<div class="chat-layout"><div class="chat-main">'
|
||||
+ '<div class="card"><h2>Chat <span id="chat-stage" class="badge" style="font-size:10px;font-weight:400;display:none"></span></h2>'
|
||||
+ '<div class="chat-messages" id="chat-msgs"><div class="empty-state" style="flex:1;display:flex;align-items:center;justify-content:center"><p>Start a conversation</p></div></div>'
|
||||
+ '<div class="chat-input-row"><input id="chat-input" placeholder="Type a message..." onkeydown="if(event.key==\'Enter\')sendChat()">'
|
||||
+ '<button class="btn btn-primary" onclick="sendChat()" id="chat-send-btn">Send</button></div></div></div>'
|
||||
+ '<div class="chat-sidebar">'
|
||||
+ '<div class="card" style="padding:12px"><h2 style="font-size:13px;margin-bottom:8px">Terminals <span id="term-count-badge" class="badge badge-blue">0</span></h2><div id="term-list" style="max-height:140px;overflow-y:auto;font-size:11px"></div></div>'
|
||||
+ '<div class="card" style="padding:12px"><h2 style="font-size:13px;margin-bottom:8px">Command History <span id="cmd-count-badge" class="badge badge-blue">0</span></h2><div id="cmd-list" style="max-height:100px;overflow-y:auto;font-size:11px"></div></div>'
|
||||
+ '<div class="card" style="padding:12px">'
|
||||
+ '<div class="sidebar-subnav"><span class="active" onclick="switchChatSub(\'memory\',this)">Memory</span><span onclick="switchChatSub(\'context\',this)">Context</span><span onclick="switchChatSub(\'knowledge\',this)">Knowledge</span></div>'
|
||||
+ '<div id="chat-sub-memory">'
|
||||
+ '<div class="kv-row"><span class="key">Entities</span><span class="val">' + (k?.memory?.entity_count || '-') + '</span></div>'
|
||||
+ '<div class="kv-row"><span class="key">Relations</span><span class="val">' + (k?.memory?.relation_count || '-') + '</span></div>'
|
||||
+ '<div style="margin-top:8px"><input id="mem-query" placeholder="Keyword query"><button class="btn btn-primary btn-sm" onclick="queryMemoryChat()">Query</button></div>'
|
||||
+ '<div id="mem-result-chat" style="margin-top:8px;max-height:160px;overflow:auto"></div></div>'
|
||||
+ '<div id="chat-sub-context" style="display:none"><div style="margin-top:8px"><input id="ctx-query" placeholder="Enter current topic">'
|
||||
+ '<button class="btn btn-primary btn-sm" onclick="queryMemoryContext()">Get Context</button></div><div id="ctx-result" style="margin-top:8px;max-height:180px;overflow:auto"></div></div>'
|
||||
+ '<div id="chat-sub-knowledge" style="display:none">'
|
||||
+ '<div class="kv-row"><span class="key">Items</span><span class="val">' + (k?.knowledge?.item_count || '-') + '</span></div>'
|
||||
+ '<div style="margin-top:8px"><input id="know-query" placeholder="Search knowledge"><button class="btn btn-primary btn-sm" onclick="searchKnowledgeChat()">Search</button></div>'
|
||||
+ '<div id="know-result-chat" style="margin-top:8px;max-height:140px;overflow:auto"></div>'
|
||||
+ '<div style="margin-top:12px;border-top:1px solid var(--border-color);padding-top:8px">'
|
||||
+ '<input id="know-name" placeholder="Knowledge name" style="margin-bottom:4px">'
|
||||
+ '<textarea id="know-content" placeholder="Content" style="min-height:50px;margin-bottom:4px"></textarea>'
|
||||
+ '<button class="btn btn-primary btn-sm" onclick="createKnowledgeChat()">Create</button></div></div></div></div></div>';
|
||||
_chatLayoutBuilt = true;
|
||||
}
|
||||
|
||||
function renderChat() {
|
||||
if (!_chatLayoutBuilt) { buildChatLayout(); renderTerminalsList(); renderCmdHistoryList(); }
|
||||
const msgsEl = document.getElementById('chat-msgs');
|
||||
if (!msgsEl) return;
|
||||
if (state.messages.length === 0) {
|
||||
msgsEl.innerHTML = '<div class="empty-state" style="flex:1;display:flex;align-items:center;justify-content:center"><p>Start a conversation</p></div>'; return;
|
||||
}
|
||||
let html = '';
|
||||
state.messages.forEach(function(m) {
|
||||
const role = m.role || 'user'; let c = m.content || '';
|
||||
if (role === 'assistant') { c = renderMarkdown(c) } else { c = '<pre>' + escHtml(c) + '</pre>' }
|
||||
const rc = m.reasoning_content ? '<div class="reasoning"><div class="reasoning-title" onclick="var n=this.nextElementSibling;n.style.display=n.style.display===\'none\'?\'block\':\'none\';this.textContent=this.textContent===\'Collapse\'?\'Expand\':\'Collapse\'">Collapse</div><div class="reasoning-body" style="display:none">' + renderMarkdown(m.reasoning_content) + '</div></div>' : '';
|
||||
html += '<div class="msg msg-' + role + '"><div class="msg-avatar">' + (role === 'user' ? 'U' : 'A') + '</div>'
|
||||
+ '<div class="msg-content"><div class="msg-bubble">' + rc + '<div class="text">' + c + '</div></div></div></div>';
|
||||
});
|
||||
msgsEl.innerHTML = html; msgsEl.scrollTop = msgsEl.scrollHeight;
|
||||
updateChatStageBadge();
|
||||
}
|
||||
|
||||
function switchChatSub(name, el) {
|
||||
document.querySelectorAll('.sidebar-subnav span').forEach(function(e) { e.classList.remove('active') });
|
||||
if (el) el.classList.add('active');
|
||||
['memory','context','knowledge'].forEach(function(s) { document.getElementById('chat-sub-' + s).style.display = s === name ? 'block' : 'none' });
|
||||
}
|
||||
|
||||
async function sendChat() {
|
||||
const inp = document.getElementById('chat-input'); const btn = document.getElementById('chat-send-btn');
|
||||
const text = inp.value.trim(); if (!text || state.chatLoading) return;
|
||||
state.messages.push({ role: 'user', content: text }); inp.value = '';
|
||||
const streamingMsg = { role: 'assistant', content: '', reasoning_content: '', _streaming: true };
|
||||
state.messages.push(streamingMsg); renderChat();
|
||||
state.chatLoading = true; btn.disabled = true; btn.textContent = '...';
|
||||
try {
|
||||
const r = await api('/chat', { method: 'POST', body: JSON.stringify({ message: text }) });
|
||||
if (streamingMsg._streaming) {
|
||||
streamingMsg.content = r.response || '(no response)'; streamingMsg.reasoning_content = r.reasoning_content || '';
|
||||
} else {
|
||||
streamingMsg.content = (streamingMsg.content || '') + (r.response || ''); streamingMsg.reasoning_content = (streamingMsg.reasoning_content || '') + (r.reasoning_content || '');
|
||||
}
|
||||
delete streamingMsg._streaming; saveChatHistory(); renderChat();
|
||||
} catch(e) {
|
||||
if (streamingMsg._streaming) { streamingMsg.content = 'Error: ' + e.message; delete streamingMsg._streaming; }
|
||||
renderChat(); toast('Request failed: ' + e.message, true);
|
||||
} finally {
|
||||
state.chatLoading = false; btn.disabled = false; btn.textContent = 'Send'; renderChat();
|
||||
}
|
||||
}
|
||||
|
||||
function updateChatStageBadge() {
|
||||
const badge = document.getElementById('chat-stage');
|
||||
if (!badge) return; badge.textContent = state.chatStage || ''; badge.style.display = state.chatStage ? 'inline' : 'none';
|
||||
}
|
||||
|
||||
function rerenderChatIfActive() {
|
||||
const tab = document.getElementById('tab-chat');
|
||||
if (tab && tab.classList.contains('active')) renderChat();
|
||||
}
|
||||
|
||||
async function loadSidebarData() {
|
||||
try { const d = await api('/terminals'); _terminals = d?.terminals || []; renderTerminalsList() } catch(e) {}
|
||||
try { const d = await api('/cmd/history'); _cmdHistory = d?.history || []; renderCmdHistoryList() } catch(e) {}
|
||||
}
|
||||
|
||||
function renderTerminalsList() {
|
||||
const el = document.getElementById('term-list'); const badge = document.getElementById('term-count-badge');
|
||||
if (!el) return; if (badge) badge.textContent = _terminals.length;
|
||||
if (_terminals.length === 0) { el.innerHTML = '<p style="color:var(--text-muted);font-size:11px">No active terminals</p>'; return; }
|
||||
el.innerHTML = _terminals.map(function(t) {
|
||||
const status = t.running ? '<span class="status-dot dot-green"></span>' : '<span class="status-dot dot-gray"></span>';
|
||||
return '<div style="padding:3px 0;border-bottom:1px solid var(--border-color);font-size:11px">' + status + ' ' + escHtml((t.command || t.id || '').substring(0, 40)) + ' <span style="color:var(--text-muted)">' + (t.uptime || '') + '</span></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderCmdHistoryList() {
|
||||
const el = document.getElementById('cmd-list'); const badge = document.getElementById('cmd-count-badge');
|
||||
if (!el) return; if (badge) badge.textContent = _cmdHistory.length;
|
||||
if (_cmdHistory.length === 0) { el.innerHTML = '<p style="color:var(--text-muted);font-size:11px">No command history</p>'; return; }
|
||||
el.innerHTML = _cmdHistory.slice(-10).reverse().map(function(c) {
|
||||
const status = c.status === 'completed' ? '<span class="badge badge-green">OK</span>' : '<span class="badge badge-red">' + escHtml(c.status || 'FAIL') + '</span>';
|
||||
return '<div style="padding:3px 0;border-bottom:1px solid var(--kv-border);font-size:11px;display:flex;justify-content:space-between"><span>' + escHtml((c.command || '').substring(0, 40)) + '</span><span>' + status + '</span></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ===== Chat History Persistence =====
|
||||
async function saveChatHistory() {
|
||||
try {
|
||||
const msgs = state.messages.filter(function(m) { return !m._streaming }).map(function(m) {
|
||||
return { role: m.role, content: m.content, reasoning_content: m.reasoning_content, time: new Date().toISOString() };
|
||||
}).slice(-100);
|
||||
await api('/settings', { method: 'PUT', body: JSON.stringify({ key: 'plugin.webui.chathistory', value: JSON.stringify(msgs) }) });
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
async function loadChatHistory() {
|
||||
if (state._chatHistoryLoaded || !state.currentConn) return;
|
||||
try {
|
||||
const s = await api('/settings?prefix=plugin.webui');
|
||||
const raw = s?.settings?.['plugin.webui.chathistory'];
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
state.messages = parsed.map(function(m) { return { role: m.role, content: m.content || '', reasoning_content: m.reasoning_content || '' } });
|
||||
state._chatHistoryLoaded = true;
|
||||
renderChat();
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
state._chatHistoryLoaded = true;
|
||||
}
|
||||
|
||||
// hook into renderChat init
|
||||
const _origRenderChat = renderChat;
|
||||
renderChat = function() {
|
||||
loadChatHistory();
|
||||
return _origRenderChat.apply(this, arguments);
|
||||
};
|
||||
|
||||
async function queryMemoryChat() {
|
||||
const q = document.getElementById('mem-query')?.value; const r = document.getElementById('mem-result-chat');
|
||||
if (!r || !q) return; r.innerHTML = '<div class="loading"></div>';
|
||||
try { const d = await api('/memory?q=' + encodeURIComponent(q) + '&depth=2'); r.innerHTML = '<pre style="font-size:11px">' + escHtml(JSON.stringify(d, null, 2)) + '</pre>'; }
|
||||
catch(e) { r.innerHTML = '<p style="color:#fca5a5">Query failed: ' + escHtml(e.message) + '</p>'; }
|
||||
}
|
||||
|
||||
async function queryMemoryContext() {
|
||||
const q = document.getElementById('ctx-query')?.value; const r = document.getElementById('ctx-result');
|
||||
if (!r) return; r.innerHTML = '<div class="loading"></div>';
|
||||
try {
|
||||
const d = await api('/memory/context?q=' + encodeURIComponent(q || ''));
|
||||
let html = '<div style="font-size:11px">';
|
||||
if (d?.summary) html += '<div class="kv-row"><span class="key">Summary</span><span class="val">' + escHtml(d.summary) + '</span></div>';
|
||||
html += '<div class="kv-row"><span class="key">Token Estimate</span><span class="val">' + (d?.token_estimate || 0) + '</span></div>';
|
||||
if (d?.entities?.length) html += '<div class="kv-row"><span class="key">Entities</span><span class="val">' + d.entities.map(function(e) { return escHtml(e.name || e.id || '') }).join(', ') + '</span></div>';
|
||||
html += '<h3 style="font-size:12px;margin:8px 0 4px">Context</h3><pre>' + escHtml(d?.context || 'No context') + '</pre></div>';
|
||||
r.innerHTML = html;
|
||||
} catch(e) { r.innerHTML = '<p style="color:#fca5a5">Query failed: ' + escHtml(e.message) + '</p>'; }
|
||||
}
|
||||
|
||||
async function searchKnowledgeChat() {
|
||||
const q = document.getElementById('know-query')?.value; const r = document.getElementById('know-result-chat');
|
||||
if (!r || !q) return; r.innerHTML = '<div class="loading"></div>';
|
||||
try { const d = await api('/knowledge?q=' + encodeURIComponent(q)); r.innerHTML = '<pre style="font-size:11px">' + escHtml(JSON.stringify(d, null, 2)) + '</pre>'; }
|
||||
catch(e) { r.innerHTML = '<p style="color:#fca5a5">Search failed: ' + escHtml(e.message) + '</p>'; }
|
||||
}
|
||||
|
||||
async function createKnowledgeChat() {
|
||||
const name = document.getElementById('know-name')?.value; const content = document.getElementById('know-content')?.value;
|
||||
if (!name || !content) { toast('Name and content required', true); return; }
|
||||
try { await api('/knowledge', { method: 'POST', body: JSON.stringify({ name, content }) }); toast('Knowledge created'); document.getElementById('know-name').value = ''; document.getElementById('know-content').value = ''; }
|
||||
catch(e) { toast('Create failed: ' + e.message, true); }
|
||||
}
|
||||
|
||||
// ===== Plugins =====
|
||||
async function renderPlugins() {
|
||||
const list = state.installedPlugins?.plugins || []; const info = state.pluginInfo;
|
||||
let html = '<div class="card"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">'
|
||||
+ '<h2 style="margin-bottom:0">Installed Plugins</h2><div style="display:flex;gap:8px">'
|
||||
+ '<button class="btn btn-ghost btn-sm" onclick="document.getElementById(\'plugin-file-input\').click()">Upload .hmap</button>'
|
||||
+ '<input type="file" id="plugin-file-input" accept=".hmap,.so,.dll" style="display:none" onchange="installPluginFile(this.files[0])">'
|
||||
+ '<button class="btn btn-ghost btn-sm" onclick="reloadPlugins()">Reload</button></div></div>';
|
||||
if (list.length === 0) { html += '<p class="empty-state">No plugins installed</p>'; }
|
||||
else {
|
||||
html += '<table><tr><th>Name</th><th>Type</th><th>Status</th><th></th></tr>';
|
||||
list.forEach(function(p) {
|
||||
const status = p.loaded ? '<span class="badge badge-green">Loaded</span>' : '<span class="badge badge-red">Error</span>';
|
||||
html += '<tr><td>' + escHtml(p.name || '') + '</td><td>' + escHtml(p.type || '') + '</td><td>' + status + '</td>'
|
||||
+ '<td><button class="btn btn-ghost btn-sm" onclick="showPluginInfo(\'' + p.name + '\')">Info</button></td></tr>';
|
||||
}); html += '</table>';
|
||||
}
|
||||
html += '</div>';
|
||||
if (info) {
|
||||
html += '<div class="card"><h2>' + escHtml(info.name || '') + ' Details</h2><pre>' + escHtml(JSON.stringify(info, null, 2)) + '</pre>'
|
||||
+ '<button class="btn btn-ghost btn-sm" onclick="state.pluginInfo=null;renderPlugins()" style="margin-top:8px">Close</button></div>';
|
||||
}
|
||||
document.getElementById('tab-plugins').innerHTML = html;
|
||||
}
|
||||
|
||||
async function showPluginInfo(name) {
|
||||
try { const d = await api('/plugins/' + encodeURIComponent(name)); state.pluginInfo = d; renderPlugins(); }
|
||||
catch(e) { toast('Failed: ' + e.message, true); }
|
||||
}
|
||||
|
||||
async function installPluginFile(file) {
|
||||
if (!file) return;
|
||||
try {
|
||||
const form = new FormData(); form.append('plugin', file);
|
||||
await fetch(state.currentConn.url + '/api/v1/plugins', { method: 'POST', body: form, headers: state.currentConn.apiKey ? { 'X-API-Key': state.currentConn.apiKey } : {} });
|
||||
toast('Plugin uploaded'); state.installedPlugins = await api('/plugins'); renderPlugins();
|
||||
} catch(e) { toast('Upload failed: ' + e.message, true); }
|
||||
}
|
||||
|
||||
async function reloadPlugins() {
|
||||
try { await api('/plugins/reload', { method: 'POST' }); state.kernel = await api('/kernel'); state.installedPlugins = await api('/plugins'); toast('Plugins reloaded'); renderPlugins(); renderOverview(); }
|
||||
catch(e) { toast('Reload failed: ' + e.message, true); }
|
||||
}
|
||||
|
||||
// ===== Knowledge Browser =====
|
||||
async function renderKnowledgeBrowser() {
|
||||
if (document.getElementById('tab-knowledge')) return;
|
||||
const tab = document.getElementById('tab-kernel');
|
||||
if (!tab || !tab.classList.contains('active')) return;
|
||||
const cont = document.getElementById('knowledge-browser');
|
||||
if (!cont) return;
|
||||
try {
|
||||
const d = await api('/knowledge');
|
||||
let html = '<div class="card"><h2>Knowledge Base</h2>';
|
||||
if (d?.categories) {
|
||||
html += '<table><tr><th>Name</th><th>Size</th></tr>';
|
||||
(d.categories || []).forEach(function(c) {
|
||||
html += '<tr><td>' + escHtml(c.name || c) + '</td><td>' + (c.content_length || '-') + '</td></tr>';
|
||||
});
|
||||
html += '</table>';
|
||||
}
|
||||
if (d?.stats) {
|
||||
html += '<div class="grid-3" style="margin-top:12px">' + statCard('Categories', d.stats.categories || 0) + statCard('Items', d.stats.items || 0) + statCard('Size', d.stats.size || 0) + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
cont.innerHTML = html;
|
||||
} catch(e) { cont.innerHTML = '<p style="color:#fca5a5">' + escHtml(e.message) + '</p>'; }
|
||||
}
|
||||
|
||||
// ===== Settings =====
|
||||
function renderOneSettings() {
|
||||
const section = state.selectedSection || 'core';
|
||||
const isPlugin = section.startsWith('plugin.'); const prefix = isPlugin ? section : (section === 'core' ? '' : section);
|
||||
const values = {}; const meta = {};
|
||||
if (isPlugin) { const pname = section.substring(7); Object.entries(state.settings).filter(function(e) { return e[0].startsWith('plugin.' + pname + '.') }).forEach(function(e) { values[e[0]] = e[1] }) }
|
||||
else if (section === 'core') { Object.entries(state.settings).filter(function(e) { return !e[0].startsWith('plugin.') }).forEach(function(e) { values[e[0]] = e[1] }) }
|
||||
else { Object.entries(state.settings).filter(function(e) { return e[0].startsWith(section + '.') || (!e[0].startsWith('plugin.') && e[0].startsWith(section)) }).forEach(function(e) { values[e[0]] = e[1] }) }
|
||||
Object.assign(meta, state.meta);
|
||||
let html = '<div class="card"><h2>' + (isPlugin ? 'Plugin: ' + section.substring(7) : 'Core Settings') + '</h2>';
|
||||
const keys = Object.keys(values);
|
||||
if (keys.length === 0) { html += '<p class="empty-state">No settings</p>' }
|
||||
else {
|
||||
keys.sort().forEach(function(k) {
|
||||
const v = values[k]; const m = meta[k]; const display = m?.displayName || k.split('.').pop() || k; const desc = m?.description || '';
|
||||
html += '<div style="margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--kv-border)"><div class="settings-key">' + escHtml(k) + '</div><label>' + escHtml(display) + '</label>'
|
||||
+ '<input value="' + escHtml(v != null ? String(v) : '') + '" onchange="saveSetting(\'' + escHtml(k) + '\', this.value)" placeholder="' + escHtml(desc) + '">'
|
||||
+ (desc ? '<div style="font-size:10px;color:var(--text-muted);margin-top:-8px;margin-bottom:4px">' + escHtml(desc) + '</div>' : '') + '</div>';
|
||||
});
|
||||
}
|
||||
html += '</div>';
|
||||
document.getElementById('tab-settings').innerHTML = '<div class="settings-layout"><div class="settings-sidebar">'
|
||||
+ state.settingsPlugins.map(function(p) { const label = p === 'core' ? 'Core' : p.replace('plugin.', ''); return '<a class="' + (section === p ? 'active' : '') + '" onclick="state.selectedSection=\'' + p + '\';renderOneSettings()">' + escHtml(label) + '</a>' }).join('')
|
||||
+ '</div><div class="settings-content">' + html + '</div></div>';
|
||||
}
|
||||
|
||||
async function saveSetting(key, value) {
|
||||
try { await api('/settings', { method: 'PUT', body: JSON.stringify({ key, value }) }); toast('Saved'); const s = await api('/settings'); state.settings = s.settings || {}; state.meta = s.meta || {}; renderOneSettings(); }
|
||||
catch(e) { toast('Save failed: ' + e.message, true); }
|
||||
}
|
||||
|
||||
// ===== Adapters =====
|
||||
async function renderAdapters() {
|
||||
let html = '<div class="card"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px"><h2 style="margin-bottom:0">LLM Adapters</h2><button class="btn btn-primary btn-sm" onclick="showUploadAdapter()">Upload</button></div>';
|
||||
try {
|
||||
const d = await api('/adapters'); const adapters = d?.adapters || [];
|
||||
if (adapters.length === 0) { html += '<p class="empty-state">No adapters</p>' }
|
||||
else {
|
||||
html += '<table><tr><th>Name</th><th>Type</th><th></th></tr>';
|
||||
adapters.forEach(function(a) { html += '<tr><td>' + escHtml(a.name || a) + '</td><td>' + escHtml(a.type || 'lua') + '</td><td><button class="btn btn-danger btn-sm" onclick="deleteAdapter(\'' + escHtml(a.name || a) + '\')">Delete</button></td></tr>' });
|
||||
html += '</table>';
|
||||
}
|
||||
} catch(e) { html += '<p class="empty-state">Failed to load: ' + escHtml(e.message) + '</p>' }
|
||||
html += '</div>';
|
||||
document.getElementById('tab-adapters').innerHTML = html;
|
||||
}
|
||||
|
||||
function showUploadAdapter() { const name = prompt('Adapter name:'); if (!name) return; const code = prompt('Paste Lua adapter code:'); if (!code) return; uploadAdapter(name, code); }
|
||||
async function uploadAdapter(name, code) { try { await api('/adapters', { method: 'POST', body: JSON.stringify({ name, code }) }); toast('Uploaded'); renderAdapters(); } catch(e) { toast('Failed: ' + e.message, true); } }
|
||||
async function deleteAdapter(name) { if (!confirm('Delete: ' + name + '?')) return; try { await api('/adapters/' + encodeURIComponent(name), { method: 'DELETE' }); toast('Deleted'); renderAdapters(); } catch(e) { toast('Failed: ' + e.message, true); } }
|
||||
|
||||
// ===== Kernel =====
|
||||
async function renderKernel() {
|
||||
const k = state.kernel;
|
||||
let html = '<div class="card"><h2>Kernel Status</h2>';
|
||||
if (!k) { html += '<p class="empty-state">Unavailable</p>' }
|
||||
else {
|
||||
html += '<div class="grid-2"><div><h3>LLM</h3><div class="kv-row"><span class="key">Provider</span><span class="val">' + escHtml(k.llm?.provider || '-') + '</span></div><div class="kv-row"><span class="key">Sources</span><span class="val">' + (k.llm?.sources || 0) + '</span></div><div class="kv-row"><span class="key">Available</span><span class="val"><span class="status-dot ' + (k.llm?.available ? 'dot-green' : 'dot-red') + '"></span>' + (k.llm?.available ? 'Yes' : 'No') + '</span></div></div>'
|
||||
+ '<div><h3>Memory</h3><div class="kv-row"><span class="key">Available</span><span class="val"><span class="status-dot ' + (k.memory?.available ? 'dot-green' : 'dot-gray') + '"></span>' + (k.memory?.available ? 'Yes' : 'No') + '</span></div>'
|
||||
+ (k.memory?.available ? '<div class="kv-row"><span class="key">Entities</span><span class="val">' + k.memory.entity_count + '</span></div><div class="kv-row"><span class="key">Relations</span><span class="val">' + k.memory.relation_count + '</span></div>' : '') + '</div></div>';
|
||||
html += '<h3>Runtime</h3><div class="kv-row"><span class="key">Goroutines</span><span class="val">' + (k.runtime?.goroutines || '-') + '</span></div><div class="kv-row"><span class="key">Memory</span><span class="val">' + (k.runtime?.memory_mb || '-') + ' MB</span></div><div class="kv-row"><span class="key">Go Version</span><span class="val">' + escHtml(k.runtime?.go_version || '-') + '</span></div>';
|
||||
html += '<h3>Plugins</h3>';
|
||||
if (k.plugins && k.plugins.length > 0) { html += '<div>' + k.plugins.map(function(p) { return '<span class="badge badge-blue" style="margin:2px">' + escHtml(p.name || p) + '</span>' }).join('') + '</div>' }
|
||||
}
|
||||
html += '</div><div class="card"><h2>Actions</h2><button class="btn btn-primary" onclick="runHealthcheck()" style="margin-right:8px">Run Healthcheck</button>'
|
||||
+ '<button class="btn btn-ghost" onclick="loadTextMemory()">View Text Memory</button></div>'
|
||||
+ '<div id="health-result"></div><div id="text-memory-result"></div>'
|
||||
+ '<div id="knowledge-browser"></div>';
|
||||
document.getElementById('tab-kernel').innerHTML = html;
|
||||
renderKnowledgeBrowser();
|
||||
}
|
||||
|
||||
async function runHealthcheck() {
|
||||
const el = document.getElementById('health-result'); el.innerHTML = '<div class="loading-spinner"></div>';
|
||||
try { const r = await api('/chat', { method: 'POST', body: JSON.stringify({ message: 'Please run the healthcheck tool for a full system check and report the results' }) }); el.innerHTML = '<div class="card"><h2>Healthcheck Result</h2><pre>' + escHtml(r.response || '') + '</pre></div>'; }
|
||||
catch(e) { el.innerHTML = '<div class="card"><p style="color:#fca5a5">' + escHtml(e.message) + '</p></div>'; }
|
||||
}
|
||||
async function loadTextMemory() {
|
||||
const el = document.getElementById('text-memory-result'); el.innerHTML = '<div class="loading-spinner"></div>';
|
||||
try { const d = await api('/memory/text'); el.innerHTML = '<div class="card"><h2>Text Memory</h2><pre>' + escHtml(JSON.stringify(d, null, 2)) + '</pre></div>'; }
|
||||
catch(e) { el.innerHTML = '<div class="card"><p style="color:#fca5a5">' + escHtml(e.message) + '</p></div>'; }
|
||||
}
|
||||
|
||||
// ===== Init =====
|
||||
document.addEventListener('DOMContentLoaded', initApp);
|
||||
68
gui/renderer/index.html
Normal file
68
gui/renderer/index.html
Normal file
@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HomeAgent</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Connection Manager Overlay -->
|
||||
<div id="conn-overlay" class="overlay">
|
||||
<div class="overlay-content conn-manager">
|
||||
<h2>连接管理 / Connections</h2>
|
||||
<div class="conn-list" id="conn-list"></div>
|
||||
<div class="conn-form" id="conn-form" style="display:none">
|
||||
<h3 id="conn-form-title">添加连接 / Add Connection</h3>
|
||||
<label>名称 / Name</label>
|
||||
<input id="conn-name" placeholder="My HomeAgent">
|
||||
<label>地址 / URL</label>
|
||||
<input id="conn-url" placeholder="http://localhost:8080">
|
||||
<label>API 密钥 / API Key</label>
|
||||
<input id="conn-key" type="password" placeholder="sk-...">
|
||||
<div class="conn-form-actions">
|
||||
<button class="btn btn-ghost" onclick="cancelConnForm()">取消 / Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveConnForm()">保存 / Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="showConnForm()" id="conn-add-btn" style="margin-top:12px">+ 添加连接 / Add Connection</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main App -->
|
||||
<div id="app" style="display:none">
|
||||
<!-- Top Bar -->
|
||||
<nav>
|
||||
<h1>HomeAgent</h1>
|
||||
<a class="active" onclick="switchTab('overview')">概览</a>
|
||||
<a onclick="switchTab('chat')">对话</a>
|
||||
<a onclick="switchTab('plugins')">插件</a>
|
||||
<a onclick="switchTab('settings')">设置</a>
|
||||
<a onclick="switchTab('adapters')">适配器</a>
|
||||
<a onclick="switchTab('kernel')">内核</a>
|
||||
<div style="margin-left:auto;display:flex;align-items:center;gap:8px">
|
||||
<span id="conn-status" class="conn-indicator" onclick="openConnManager()" title="点击管理连接">
|
||||
<span class="status-dot dot-gray" id="conn-dot"></span>
|
||||
<span id="conn-name-display">未连接</span>
|
||||
<span style="font-size:10px;margin-left:4px;opacity:0.6">▼</span>
|
||||
</span>
|
||||
<button class="theme-btn" onclick="toggleTheme()" id="theme-btn">🌙</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container" id="content">
|
||||
<div id="tab-overview" class="tab-content active"></div>
|
||||
<div id="tab-chat" class="tab-content"></div>
|
||||
<div id="tab-plugins" class="tab-content"></div>
|
||||
<div id="tab-settings" class="tab-content"></div>
|
||||
<div id="tab-adapters" class="tab-content"></div>
|
||||
<div id="tab-kernel" class="tab-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
203
gui/renderer/style.css
Normal file
203
gui/renderer/style.css
Normal file
@ -0,0 +1,203 @@
|
||||
:root {
|
||||
--bg-primary: #0f172a;
|
||||
--bg-secondary: #1e293b;
|
||||
--bg-card: #1e293b;
|
||||
--bg-input: #0f172a;
|
||||
--bg-hover: rgba(15,23,42,0.25);
|
||||
--text-primary: #e2e8f0;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
--border-color: #334155;
|
||||
--accent: #38bdf8;
|
||||
--accent-bg: #1e3a5f;
|
||||
--toast-bg: #166534;
|
||||
--toast-color: #86efac;
|
||||
--toast-error-bg: #7f1d1d;
|
||||
--toast-error-color: #fca5a5;
|
||||
--pre-color: #a5b4fc;
|
||||
--pre-bg: #0f172a;
|
||||
--chat-bg: #0f172a;
|
||||
--msg-user-bg: #1e3a5f;
|
||||
--msg-user-color: #93c5fd;
|
||||
--msg-assistant-bg: #1a3a2a;
|
||||
--msg-assistant-color: #86efac;
|
||||
--msg-system-bg: #3b1a3a;
|
||||
--msg-system-color: #f0abfc;
|
||||
--kv-border: #1e293b;
|
||||
}
|
||||
[data-theme=light] {
|
||||
--bg-primary: #f8fafc;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-card: #ffffff;
|
||||
--bg-input: #f1f5f9;
|
||||
--bg-hover: rgba(241,245,249,0.8);
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--text-muted: #94a3b8;
|
||||
--border-color: #e2e8f0;
|
||||
--accent: #2563eb;
|
||||
--accent-bg: #dbeafe;
|
||||
--pre-color: #1e293b;
|
||||
--pre-bg: #f1f5f9;
|
||||
--chat-bg: #f1f5f9;
|
||||
--msg-user-bg: #dbeafe;
|
||||
--msg-user-color: #1e40af;
|
||||
--msg-assistant-bg: #dcfce7;
|
||||
--msg-assistant-color: #166534;
|
||||
--msg-system-bg: #f3e8ff;
|
||||
--msg-system-color: #7c3aed;
|
||||
--kv-border: #e2e8f0;
|
||||
}
|
||||
* { margin:0; padding:0; box-sizing:border-box; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif }
|
||||
body { background:var(--bg-primary); color:var(--text-primary); min-height:100vh; transition:background .2s,color .2s }
|
||||
nav { background:var(--bg-secondary); padding:0 24px; display:flex; align-items:center; gap:4px; border-bottom:1px solid var(--border-color); height:48px; position:sticky; top:0; z-index:100 }
|
||||
nav h1 { font-size:16px; font-weight:700; color:var(--accent); margin-right:24px; white-space:nowrap }
|
||||
nav a { padding:12px 16px; color:var(--text-secondary); text-decoration:none; font-size:13px; cursor:pointer; border-bottom:2px solid transparent; transition:all .15s }
|
||||
nav a:hover { color:var(--text-primary) }
|
||||
nav a.active { color:var(--accent); border-bottom-color:var(--accent) }
|
||||
.theme-btn { background:none; border:1px solid var(--border-color); color:var(--text-secondary); cursor:pointer; padding:4px 8px; border-radius:6px; font-size:14px; line-height:1; transition:all .15s }
|
||||
.theme-btn:hover { color:var(--accent); border-color:var(--accent) }
|
||||
.container { padding:20px 24px; max-width:1440px; margin:0 auto }
|
||||
.card { background:var(--bg-card); border:1px solid var(--border-color); border-radius:10px; padding:20px; margin-bottom:16px; transition:background .2s,border .2s }
|
||||
.card h2 { font-size:15px; font-weight:600; margin-bottom:12px; color:var(--text-primary) }
|
||||
.card h3 { font-size:13px; font-weight:600; color:var(--text-secondary); margin:16px 0 8px }
|
||||
.grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:16px }
|
||||
.grid-3 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:16px }
|
||||
.grid-4 { display:grid; grid-template-columns:repeat(4,1fr); gap:16px }
|
||||
.stat-value { font-size:26px; font-weight:700; color:var(--accent) }
|
||||
.stat-label { font-size:11px; color:var(--text-muted); margin-top:2px }
|
||||
.stat-card { padding:16px 20px }
|
||||
.status-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px }
|
||||
.dot-green { background:#22c55e }
|
||||
.dot-yellow { background:#eab308 }
|
||||
.dot-red { background:#ef4444 }
|
||||
.dot-gray { background:#475569 }
|
||||
table { width:100%; border-collapse:collapse; font-size:13px }
|
||||
th { text-align:left; padding:8px 10px; color:var(--text-muted); font-weight:500; border-bottom:1px solid var(--border-color); font-size:11px; text-transform:uppercase; letter-spacing:.5px }
|
||||
td { padding:8px 10px; border-bottom:1px solid var(--kv-border) }
|
||||
tr:hover td { background:var(--bg-hover) }
|
||||
.badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:11px; font-weight:500 }
|
||||
.badge-green { background:#166534; color:#86efac }
|
||||
.badge-red { background:#7f1d1d; color:#fca5a5 }
|
||||
.badge-yellow { background:#713f12; color:#fde68a }
|
||||
.badge-blue { background:#1e3a5f; color:#93c5fd }
|
||||
.btn { padding:6px 14px; border-radius:6px; border:none; font-size:12px; cursor:pointer; font-weight:500; transition:all .15s }
|
||||
.btn-primary { background:var(--accent); color:#fff }
|
||||
.btn-primary:hover { filter:brightness(1.1) }
|
||||
.btn-primary:disabled { opacity:.5; cursor:not-allowed }
|
||||
.btn-danger { background:#dc2626; color:#fff }
|
||||
.btn-danger:hover { background:#b91c1c }
|
||||
.btn-sm { padding:4px 10px; font-size:11px }
|
||||
.btn-ghost { background:transparent; border:1px solid var(--border-color); color:var(--text-secondary); cursor:pointer; padding:6px 14px; border-radius:6px; font-size:12px }
|
||||
.btn-ghost:hover { background:var(--bg-hover); color:var(--text-primary) }
|
||||
.tab-content { display:none }
|
||||
.tab-content.active { display:block }
|
||||
input,textarea,select { background:var(--bg-input); border:1px solid var(--border-color); border-radius:6px; padding:8px 12px; color:var(--text-primary); font-size:13px; width:100%; margin-bottom:10px; outline:none; transition:border .15s }
|
||||
input:focus,textarea:focus,select:focus { border-color:var(--accent) }
|
||||
textarea { resize:vertical; min-height:80px; font-family:monospace; font-size:12px }
|
||||
label { display:block; font-size:11px; color:var(--text-secondary); margin-bottom:3px; font-weight:500 }
|
||||
pre { background:var(--pre-bg); border-radius:6px; padding:12px; font-size:12px; overflow-x:auto; color:var(--pre-color); font-family:monospace; max-height:400px; overflow-y:auto }
|
||||
code { font-family:monospace; font-size:12px; color:var(--pre-color) }
|
||||
.settings-layout { display:flex; gap:20px; min-height:60vh }
|
||||
.settings-sidebar { width:200px; flex-shrink:0; background:var(--bg-card); border:1px solid var(--border-color); border-radius:10px; padding:8px 0; overflow-y:auto; max-height:70vh }
|
||||
.settings-sidebar a { display:block; padding:9px 16px; color:var(--text-secondary); font-size:13px; cursor:pointer; border-left:3px solid transparent; transition:all .1s }
|
||||
.settings-sidebar a:hover { background:var(--bg-primary); color:var(--text-primary) }
|
||||
.settings-sidebar a.active { background:var(--bg-primary); color:var(--accent); border-left-color:var(--accent) }
|
||||
.settings-content { flex:1; min-width:0 }
|
||||
.settings-key { font-family:monospace; font-size:11px; color:var(--text-muted); margin-bottom:2px }
|
||||
.reasoning { border-left:2px solid #888; padding-left:12px; margin:8px 0; font-size:12px; color:#999 }
|
||||
.reasoning-title { cursor:pointer; font-size:11px; color:#666; font-weight:600; user-select:none; margin-bottom:4px }
|
||||
.reasoning-body { color:#999; line-height:1.5 }
|
||||
.msg-content .text h1,.msg-content .text h2,.msg-content .text h3 { font-size:1em; margin:8px 0 4px }
|
||||
.msg-content .text p { margin:4px 0; line-height:1.5 }
|
||||
.msg-content .text ul,.msg-content .text ol { padding-left:20px; margin:4px 0 }
|
||||
.msg-content .text li { margin:2px 0 }
|
||||
.msg-content .text code { background:var(--pre-bg); padding:1px 4px; border-radius:3px; font-size:11px }
|
||||
.msg-content .text pre { background:var(--pre-bg); border-radius:6px; padding:10px; margin:8px 0; overflow-x:auto; font-size:11px; max-height:300px }
|
||||
.msg-content .text pre code { background:none; padding:0 }
|
||||
.msg-content .text blockquote { border-left:3px solid var(--border-color); padding-left:10px; margin:8px 0; color:var(--text-secondary) }
|
||||
.msg-content .text table { border-collapse:collapse; margin:8px 0; font-size:12px; width:100% }
|
||||
.msg-content .text th,.msg-content .text td { border:1px solid var(--border-color); padding:4px 8px; text-align:left }
|
||||
.toast { position:fixed; bottom:20px; right:20px; background:var(--toast-bg); color:var(--toast-color); padding:10px 20px; border-radius:8px; font-size:13px; display:none; z-index:9999; box-shadow:0 4px 12px rgba(0,0,0,.3) }
|
||||
.toast.error { background:var(--toast-error-bg); color:var(--toast-error-color) }
|
||||
.empty-state { text-align:center; padding:40px 20px; color:var(--text-muted) }
|
||||
.chat-layout { display:flex; gap:16px; height:calc(100vh - 100px); min-height:60vh; overflow:hidden }
|
||||
.chat-main { flex:2; min-width:0; display:flex; flex-direction:column }
|
||||
.chat-main .card { flex:1; display:flex; flex-direction:column; margin-bottom:0 }
|
||||
.chat-messages { flex:1; overflow-y:auto; padding:12px; border:1px solid var(--border-color); border-radius:8px; background:var(--chat-bg); display:flex; flex-direction:column; gap:4px }
|
||||
.msg { display:flex; gap:8px; margin-bottom:2px; align-items:flex-start; max-width:85% }
|
||||
.msg-user { flex-direction:row-reverse; align-self:flex-end }
|
||||
.msg-assistant { align-self:flex-start }
|
||||
.msg-system { align-self:center; max-width:90% }
|
||||
.msg-avatar { width:28px; height:28px; border-radius:6px; display:flex; align-items:center; justify-content:center; font-size:12px; flex-shrink:0 }
|
||||
.msg-user .msg-avatar { background:var(--msg-user-bg); color:var(--msg-user-color) }
|
||||
.msg-assistant .msg-avatar { background:var(--msg-assistant-bg); color:var(--msg-assistant-color) }
|
||||
.msg-system .msg-avatar { background:var(--msg-system-bg); color:var(--msg-system-color) }
|
||||
.msg-bubble { padding:8px 12px; border-radius:10px; font-size:13px; line-height:1.5; word-break:break-word }
|
||||
.msg-user .msg-bubble { background:var(--msg-user-bg); color:var(--msg-user-color); border-bottom-right-radius:4px }
|
||||
.msg-assistant .msg-bubble { background:var(--msg-assistant-bg); color:var(--msg-assistant-color); border-bottom-left-radius:4px }
|
||||
.msg-system .msg-bubble { background:var(--msg-system-bg); color:var(--msg-system-color); text-align:center; font-size:12px }
|
||||
.msg-bubble .text { white-space:pre-wrap }
|
||||
.msg-bubble .text p { margin:4px 0 }
|
||||
.msg-bubble .text pre { background:var(--pre-bg); border-radius:6px; padding:8px; margin:4px 0; overflow-x:auto; font-size:11px; max-height:200px }
|
||||
.msg-bubble .text code { background:var(--pre-bg); padding:1px 4px; border-radius:3px; font-size:11px }
|
||||
.msg-bubble .text pre code { background:none; padding:0 }
|
||||
.msg-bubble .reasoning { border-left:2px solid rgba(255,255,255,0.2); padding-left:8px; margin:6px 0; font-size:11px; opacity:0.7 }
|
||||
.msg-bubble .reasoning-title { cursor:pointer; font-size:10px; font-weight:600; user-select:none; margin-bottom:2px }
|
||||
.msg-bubble .tool-call { background:rgba(0,0,0,0.15); border-radius:6px; padding:6px 8px; margin:4px 0; font-size:11px; border-left:2px solid var(--accent) }
|
||||
.msg-bubble .tool-call .tc-name { font-weight:600; color:var(--accent) }
|
||||
.msg-bubble .tool-call .tc-args { font-family:monospace; font-size:10px; opacity:0.7; white-space:pre-wrap; word-break:break-all; margin-top:2px }
|
||||
.msg-bubble .tool-call .tc-result { font-family:monospace; font-size:10px; opacity:0.6; white-space:pre-wrap; word-break:break-all; margin-top:2px; max-height:80px; overflow-y:auto }
|
||||
.chat-input-row { display:flex; gap:8px; flex-shrink:0; padding-top:10px }
|
||||
.chat-input-row input { flex:1; margin-bottom:0 }
|
||||
.chat-input-row button { flex-shrink:0; margin-bottom:0 }
|
||||
.chat-sidebar { flex:1; min-width:240px; max-width:340px; overflow-y:auto; display:flex; flex-direction:column; gap:12px }
|
||||
.chat-sidebar .card { margin-bottom:0 }
|
||||
.loading { display:inline-block; width:16px; height:16px; border:2px solid var(--border-color); border-radius:50%; border-top-color:var(--accent); animation:spin .6s linear infinite }
|
||||
@keyframes spin { to { transform:rotate(360deg) } }
|
||||
.kv-row { display:flex; padding:6px 0; border-bottom:1px solid var(--kv-border); font-size:13px }
|
||||
.kv-row .key { color:var(--text-muted); width:180px; flex-shrink:0 }
|
||||
.kv-row .val { color:var(--text-primary); word-break:break-all }
|
||||
.loading-spinner { width:32px; height:32px; border:3px solid rgba(68,136,255,0.15); border-top:3px solid #4488ff; border-radius:50%; animation:spin 0.8s linear infinite; margin:20px auto }
|
||||
#starmap-container { position:relative; overflow:hidden }
|
||||
#starmap-container canvas { display:block }
|
||||
#starmap-placeholder { display:flex; align-items:center; justify-content:center; height:100%; width:100% }
|
||||
.sidebar-subnav { display:flex; gap:0; border-bottom:1px solid var(--border-color); margin-bottom:10px }
|
||||
.sidebar-subnav span { padding:6px 12px; font-size:12px; cursor:pointer; color:var(--text-muted); border-bottom:2px solid transparent; transition:all .15s }
|
||||
.sidebar-subnav span:hover { color:var(--text-primary) }
|
||||
.sidebar-subnav span.active { color:var(--accent); border-bottom-color:var(--accent) }
|
||||
|
||||
/* Connection Manager */
|
||||
.overlay { position:fixed; inset:0; background:rgba(0,0,0,0.6); display:none; align-items:center; justify-content:center; z-index:1000 }
|
||||
.overlay-content { background:var(--bg-card); border:1px solid var(--border-color); border-radius:12px; padding:28px; width:520px; max-height:80vh; overflow-y:auto }
|
||||
.overlay-content h2 { font-size:18px; margin-bottom:16px; color:var(--text-primary) }
|
||||
.conn-item { display:flex; align-items:center; gap:12px; padding:12px 16px; border:1px solid var(--border-color); border-radius:8px; margin-bottom:8px; cursor:pointer; transition:all .15s }
|
||||
.conn-item:hover { background:var(--bg-hover); border-color:var(--accent) }
|
||||
.conn-item.active { border-color:var(--accent); background:var(--accent-bg) }
|
||||
.conn-item .conn-info { flex:1; min-width:0 }
|
||||
.conn-item .conn-name { font-size:14px; font-weight:600; color:var(--text-primary) }
|
||||
.conn-item .conn-url { font-size:11px; color:var(--text-muted); margin-top:2px }
|
||||
.conn-item .conn-actions { display:flex; gap:4px; flex-shrink:0 }
|
||||
.conn-form h3 { font-size:15px; margin-bottom:12px }
|
||||
.conn-form-actions { display:flex; gap:8px; justify-content:flex-end; margin-top:12px }
|
||||
.conn-indicator { display:flex; align-items:center; gap:4px; cursor:pointer; padding:4px 10px; border-radius:6px; font-size:12px; color:var(--text-secondary); transition:all .15s; user-select:none }
|
||||
.conn-indicator:hover { background:var(--bg-hover); color:var(--text-primary) }
|
||||
|
||||
@media(max-width:768px) {
|
||||
nav { padding:0 8px; gap:2px }
|
||||
nav h1 { font-size:13px; margin-right:8px }
|
||||
nav a { padding:8px 6px; font-size:11px }
|
||||
.container { padding:12px }
|
||||
.card { padding:12px }
|
||||
.grid-2,.grid-3,.grid-4 { grid-template-columns:1fr }
|
||||
.stat-value { font-size:20px }
|
||||
.settings-layout { flex-direction:column }
|
||||
.settings-sidebar { width:100%; max-height:200px; display:flex; flex-wrap:wrap; padding:4px }
|
||||
.settings-sidebar a { display:inline-block; padding:6px 12px; border-left:none; border-bottom:2px solid transparent }
|
||||
.settings-sidebar a.active { border-left:none; border-bottom-color:var(--accent) }
|
||||
.kv-row { flex-direction:column; gap:2px }
|
||||
.kv-row .key { width:auto }
|
||||
.chat-layout { flex-direction:column; height:auto }
|
||||
.chat-sidebar { max-width:none }
|
||||
.msg { max-width:95% }
|
||||
}
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -75,8 +76,9 @@ type Agent struct {
|
||||
currentOutputChannel string
|
||||
|
||||
// 阶段管道:插件消息流编辑
|
||||
stageHost *StageHost
|
||||
eventBus *events.Bus
|
||||
stageHost *StageHost
|
||||
eventBus *events.Bus
|
||||
pluginHealth *pluginHealthTracker
|
||||
|
||||
// 自循环输入通道:核心内部任务(记忆消歧、系统维护),不经过 IO 层
|
||||
selfInputCh chan string
|
||||
@ -190,6 +192,7 @@ func New(cfg AgentConfig) *Agent {
|
||||
selfInputCh: make(chan string, 64),
|
||||
childResults: make(map[string]string),
|
||||
interceptCh: make(chan *agentIO.InputEvent, 64),
|
||||
pluginHealth: newPluginHealthTracker(),
|
||||
thinkingEnabled: cfg.ThinkingEnabled,
|
||||
inputCfg: cfg.InputProcessing,
|
||||
noMergeMarkers: make(map[string]int),
|
||||
@ -227,6 +230,13 @@ func (a *Agent) injectSelf(task string) {
|
||||
}
|
||||
|
||||
func (a *Agent) eventLoop() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[agent] eventLoop panic recovered: %v\n%s", r, debug.Stack())
|
||||
time.Sleep(time.Second)
|
||||
go a.eventLoop()
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case evt := <-a.io.InputChan():
|
||||
@ -244,6 +254,13 @@ func (a *Agent) eventLoop() {
|
||||
// a) 通过 cancelLLM + interceptCh 直接打断进行中的 LLM 请求
|
||||
// b) 通过 a.io.InjectInput() → InputChan → eventLoop(代理空闲时触发新处理循环)
|
||||
func (a *Agent) interceptLoop() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[agent] interceptLoop panic recovered: %v\n%s", r, debug.Stack())
|
||||
time.Sleep(time.Second)
|
||||
go a.interceptLoop()
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case evt := <-a.io.InputInterruptChan():
|
||||
@ -619,7 +636,6 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
|
||||
a.personality != nil && a.personality.Content != "",
|
||||
a.docStoreSize())
|
||||
|
||||
// === Stage: pre_action — 上下文就绪,即将调用 LLM ===
|
||||
if a.runStage(sdk.StagePreAction, stageCtx) {
|
||||
return *stageCtx.Response, toolsUsed, nil
|
||||
}
|
||||
@ -745,6 +761,23 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
|
||||
resp.Content = stageCtx.LLMText
|
||||
resp.ToolCalls = convertBackToolCalls(stageCtx.ToolCalls)
|
||||
|
||||
// === 发布完整 LLM 响应(含 tool_calls)供插件消费(如 webui 展示) ===
|
||||
chainPayload := map[string]interface{}{
|
||||
"content": resp.Content,
|
||||
"reasoning": resp.ReasoningContent,
|
||||
"tool_calls": resp.ToolCalls,
|
||||
"phase": "intermediate",
|
||||
"turn": turn,
|
||||
}
|
||||
if resp.TokenUsage.Total > 0 {
|
||||
chainPayload["usage"] = map[string]int{
|
||||
"prompt": resp.TokenUsage.Prompt,
|
||||
"completion": resp.TokenUsage.Completion,
|
||||
"total": resp.TokenUsage.Total,
|
||||
}
|
||||
}
|
||||
a.publishEvent(events.EventAgentLLMChain, chainPayload)
|
||||
|
||||
if len(resp.ToolCalls) == 0 {
|
||||
return resp.Content, toolsUsed, nil
|
||||
}
|
||||
@ -754,7 +787,6 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
|
||||
pluginName := a.resolveToolPlugin(tc.Name)
|
||||
log.Printf("[agent] executing tool: %s (plugin=%s, id=%s)", tc.Name, pluginName, tc.ID)
|
||||
|
||||
// === Stage: before_toolcall — 插件可拒绝/改参 ===
|
||||
sdkTC := sdk.ToolCall{ID: tc.ID, Name: tc.Name, Plugin: pluginName, Arguments: tc.Arguments}
|
||||
stageCtx.ToolCalls = []sdk.ToolCall{sdkTC}
|
||||
stageCtx.ToolResults = nil
|
||||
@ -773,6 +805,14 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
|
||||
}
|
||||
tc.Arguments = stageCtx.ToolCalls[0].Arguments
|
||||
|
||||
if pluginName != "" && !a.pluginHealth.isHealthy(pluginName) {
|
||||
result := fmt.Sprintf("插件 %s 处于崩溃状态,已跳过执行,等待自动恢复重载", pluginName)
|
||||
log.Printf("[agent] skip tool %s: plugin %s unhealthy", tc.Name, pluginName)
|
||||
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{tc}})
|
||||
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result})
|
||||
continue
|
||||
}
|
||||
|
||||
result := a.executeToolCall(tc)
|
||||
log.Printf("[agent] tool %s result: %s", tc.Name, truncateStr(result, 100))
|
||||
|
||||
@ -972,7 +1012,21 @@ func (a *Agent) buildMessages(sysPrompt, input string) []agentAPI.Message {
|
||||
return msgs
|
||||
}
|
||||
|
||||
func (a *Agent) executeToolCall(tc agentAPI.ToolCall) string {
|
||||
func (a *Agent) executeToolCall(tc agentAPI.ToolCall) (ret string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stack := debug.Stack()
|
||||
log.Printf("[agent] tool %s panic: %v\n%s", tc.Name, r, stack)
|
||||
|
||||
if pluginName := a.resolveToolPlugin(tc.Name); pluginName != "" {
|
||||
if a.pluginHealth.recordCrash(pluginName) {
|
||||
log.Printf("[agent] plugin %s exceeded crash threshold, scheduling reload", pluginName)
|
||||
}
|
||||
}
|
||||
|
||||
ret = fmt.Sprintf("工具 %s 执行崩溃: %v", tc.Name, r)
|
||||
}
|
||||
}()
|
||||
switch {
|
||||
case strings.HasPrefix(tc.Name, "memory_"):
|
||||
return a.executeMemoryTool(tc)
|
||||
@ -982,7 +1036,9 @@ func (a *Agent) executeToolCall(tc agentAPI.ToolCall) string {
|
||||
return a.executeKnowledgeTool(tc)
|
||||
case strings.HasPrefix(tc.Name, "doc_"):
|
||||
return a.executeDocTool(tc)
|
||||
case tc.Name == "output_send":
|
||||
case strings.HasPrefix(tc.Name, "output_send__") && strings.HasSuffix(tc.Name, "_help"):
|
||||
return a.executeOutputSendHelp(tc)
|
||||
case strings.HasPrefix(tc.Name, "output_send__"):
|
||||
return a.executeOutputSendTool(tc)
|
||||
case tc.Name == "output_list_channels":
|
||||
return a.executeOutputListChannels()
|
||||
@ -1512,6 +1568,13 @@ func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// 输出指令:使用 output_send__{channel} 作为回复手段
|
||||
prompt += "\n\n【输出规则】你有多组输出门工具(type=output),每个对应一个输出通道。回复用户时必须调用对应的 output_send__{通道名} 工具。\n"
|
||||
prompt += "- content 参数是 JSON 字符串,包含要发送的内容。具体格式因通道而异,用 output_send__{通道名}_help 查看每个通道的 JSON 格式说明。\n"
|
||||
prompt += "- output_send__{通道名}_help 是普通 function 类型工具,调用后返回该通道的 JSON 格式详情和示例。\n"
|
||||
prompt += "- 同一轮对话中可多次调用输出门工具。长消息应当分多次发出,而不是一口气发完。\n"
|
||||
prompt += "- 直接返回纯文本不会到达任何用户端。"
|
||||
|
||||
if a.skills != nil {
|
||||
if sp := a.skills.GetInjectedPrompt(); sp != "" {
|
||||
prompt += "\n\n" + sp
|
||||
@ -1943,56 +2006,65 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
})
|
||||
}
|
||||
|
||||
// 输出通道工具 — 从已注册 Device 动态生成
|
||||
// 输出通道工具 — 每注册通道生成两个工具:
|
||||
// output_send__{name} (type=output) — 向该通道发送内容
|
||||
// output_send__{name}_help (type=function) — 查看该通道的 JSON 格式说明
|
||||
channels := a.io.ListChannels()
|
||||
chanNames := make([]interface{}, 0, len(channels))
|
||||
chanDesc := "输出通道名称: "
|
||||
for i, ch := range channels {
|
||||
if ch.Type == agentIO.DeviceOutput || ch.Type == agentIO.DeviceIO {
|
||||
chanNames = append(chanNames, ch.Name)
|
||||
if i > 0 {
|
||||
chanDesc += ", "
|
||||
}
|
||||
chanDesc += ch.Name
|
||||
for _, ch := range channels {
|
||||
if ch.Type != agentIO.DeviceOutput && ch.Type != agentIO.DeviceIO {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(chanNames) == 0 {
|
||||
chanNames = []interface{}{"default"}
|
||||
chanDesc = "输出通道名称: default"
|
||||
capStr := a.io.GetChannelCapabilities(ch.Name).String()
|
||||
desc := ch.Description
|
||||
if desc == "" {
|
||||
desc = ch.Name + " 输出通道"
|
||||
}
|
||||
|
||||
// 输出门工具
|
||||
tools = append(tools, map[string]interface{}{
|
||||
"type": "output",
|
||||
"function": map[string]interface{}{
|
||||
"name": "output_send__" + ch.Name,
|
||||
"description": desc + "。能力: " + capStr + "。content 参数为 JSON 字符串,具体格式请调用 output_send__" + ch.Name + "_help 查看。",
|
||||
"parameters": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"content": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "JSON 字符串,包含要发送的内容和路由信息。格式因通道而异,用 _help 工具查看详情。",
|
||||
},
|
||||
},
|
||||
"required": []string{"content"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 帮助工具
|
||||
tools = append(tools, map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": "output_send__" + ch.Name + "_help",
|
||||
"description": "查看 " + ch.Name + " 输出通道的 JSON 格式说明和示例",
|
||||
"parameters": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// output_list_channels — 列出所有可用输出通道
|
||||
tools = append(tools, map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": "output_list_channels",
|
||||
"description": "列出所有可用输出通道及其能力(如 text/file/image/audio)和可调用工具。",
|
||||
"description": "列出所有可用输出通道及其能力(如 text/file/image/audio)和对应的输出门工具名称。",
|
||||
"parameters": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
})
|
||||
tools = append(tools, map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": "output_send",
|
||||
"description": "通过指定输出通道立即发送一条消息,不等待主回复。用于异步通知、中间进度等场景。",
|
||||
"parameters": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"channel": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": chanDesc,
|
||||
},
|
||||
"content": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "消息内容",
|
||||
},
|
||||
},
|
||||
"required": []string{"channel", "content"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 媒体处理工具:仅当本轮有未处理的媒体数据时注册
|
||||
if a.pendingMedia != nil {
|
||||
@ -2075,6 +2147,13 @@ func (a *Agent) enqueueConsolidationTask(task ConsolidationTask) {
|
||||
|
||||
// distillLoop — 定期心跳:上下文→文档 + 图→文档 + 图重整
|
||||
func (a *Agent) distillLoop() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[agent] distillLoop panic recovered: %v\n%s", r, debug.Stack())
|
||||
time.Sleep(time.Second)
|
||||
go a.distillLoop()
|
||||
}
|
||||
}()
|
||||
if a.docStore == nil && a.memory == nil {
|
||||
return
|
||||
}
|
||||
@ -2088,6 +2167,7 @@ func (a *Agent) distillLoop() {
|
||||
a.distillContext()
|
||||
a.syncGraphToDocs()
|
||||
a.reorgGraph()
|
||||
a.autoReloadPlugins()
|
||||
case <-a.ctx.Done():
|
||||
return
|
||||
}
|
||||
@ -2461,24 +2541,90 @@ func (a *Agent) processConsolidation(input string) {
|
||||
|
||||
// executeOutputSendTool — AI 通过指定通道发送消息(校验通道能力)
|
||||
func (a *Agent) executeOutputSendTool(tc agentAPI.ToolCall) string {
|
||||
channel, _ := tc.Arguments["channel"].(string)
|
||||
// tool name is "output_send__{channel}"
|
||||
channel := strings.TrimPrefix(tc.Name, "output_send__")
|
||||
content, _ := tc.Arguments["content"].(string)
|
||||
if channel == "" || content == "" {
|
||||
return "channel 和 content 不能为空"
|
||||
return "工具名称格式: output_send__{channel},content 不能为空"
|
||||
}
|
||||
|
||||
// content 是一个 JSON 字符串,插件通过解析它确定如何发送消息
|
||||
// === Stage: before_output — 输出前插件可审查/改写/拦截 ===
|
||||
stageCtx := &sdk.StageContext{
|
||||
FinalText: content,
|
||||
Phase: sdk.StageBeforeOutput,
|
||||
}
|
||||
a.runStage(sdk.StageBeforeOutput, stageCtx)
|
||||
if stageCtx.Response != nil {
|
||||
return fmt.Sprintf("输出被插件拦截: %s", *stageCtx.Response)
|
||||
}
|
||||
content = stageCtx.FinalText
|
||||
if content == "" {
|
||||
return "输出被插件清空"
|
||||
}
|
||||
tc.Arguments["content"] = content
|
||||
|
||||
// 通道能力检查
|
||||
caps := a.io.GetChannelCapabilities(channel)
|
||||
if caps == 0 {
|
||||
return fmt.Sprintf("通道 [%s] 不存在或不可用。可用通道请用 output_list_channels 查看", channel)
|
||||
return fmt.Sprintf("通道 [%s] 不存在或不可用。可用输出工具列表见 output_list_channels", channel)
|
||||
}
|
||||
if !caps.Supports(agentIO.CapText) {
|
||||
return fmt.Sprintf("通道 [%s] 不支持文本输出(能力: %s)", channel, caps.String())
|
||||
}
|
||||
|
||||
// 通过设备处理器投递
|
||||
if dev := a.io.GetDevice(channel); dev != nil {
|
||||
result, err := dev.Execute("output", tc.Arguments)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("通过 [%s] 通道发送失败: %v", channel, err)
|
||||
}
|
||||
return fmt.Sprintf("已通过 [%s] 通道发送: %v", channel, result)
|
||||
}
|
||||
|
||||
// 降级:发送到 outputCh(供 OutputChan 消费者)
|
||||
a.io.EmitTextTo("agent_io", channel, content)
|
||||
return fmt.Sprintf("已通过 [%s] 通道发送", channel)
|
||||
}
|
||||
|
||||
// executeOutputSendHelp — 返回指定通道的 JSON 格式说明
|
||||
func (a *Agent) executeOutputSendHelp(tc agentAPI.ToolCall) string {
|
||||
// tool name is "output_send__{channel}_help"
|
||||
suffix := strings.TrimPrefix(tc.Name, "output_send__")
|
||||
channel := strings.TrimSuffix(suffix, "_help")
|
||||
if channel == "" {
|
||||
return "工具名称格式: output_send__{channel}_help"
|
||||
}
|
||||
|
||||
dev := a.io.GetDevice(channel)
|
||||
if dev == nil {
|
||||
return fmt.Sprintf("通道 [%s] 不存在", channel)
|
||||
}
|
||||
|
||||
caps := a.io.GetChannelCapabilities(channel)
|
||||
capStr := "无"
|
||||
if caps != 0 {
|
||||
capStr = caps.String()
|
||||
}
|
||||
|
||||
desc := dev.Description()
|
||||
if desc == "" {
|
||||
desc = channel + " 输出通道"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`通道 [%s]
|
||||
描述: %s
|
||||
能力: %s
|
||||
|
||||
【content JSON 格式说明】
|
||||
发送到此通道时 content 必须是 JSON 字符串,包含以下字段:
|
||||
- "content": 消息正文(必填)
|
||||
- 根据通道不同可能还需要路由字段(如 "group_id", "user_id" 等)
|
||||
|
||||
请在通道描述中查看具体字段要求。
|
||||
示例: {"content":"你好"}`, channel, desc, capStr)
|
||||
}
|
||||
|
||||
// executeOutputListChannels — 列出所有可用通道及其能力
|
||||
func (a *Agent) executeOutputListChannels() string {
|
||||
channels := a.io.ListChannels()
|
||||
@ -2520,6 +2666,28 @@ func (a *Agent) executePluginReload() string {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (a *Agent) autoReloadPlugins() {
|
||||
if a.pluginReg == nil {
|
||||
return
|
||||
}
|
||||
for _, name := range a.pluginHealth.pendingReloads() {
|
||||
if !a.pluginReg.AutoRestartEnabled(name) {
|
||||
log.Printf("[agent] skip auto-reload plugin %s: auto-restart disabled by plugin", name)
|
||||
continue
|
||||
}
|
||||
log.Printf("[agent] auto-reloading unhealthy plugin: %s", name)
|
||||
if a.stageHost != nil {
|
||||
a.stageHost.UnregisterPluginTools(name)
|
||||
}
|
||||
if err := a.pluginReg.ReloadOne(name); err != nil {
|
||||
log.Printf("[agent] auto-reload plugin %s failed: %v", name, err)
|
||||
} else {
|
||||
a.pluginHealth.markReloaded(name)
|
||||
log.Printf("[agent] plugin %s reloaded successfully", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeSpawnChild 创建子 Agent 异步执行独立任务
|
||||
// 不阻塞主 Agent,子任务完成后通过 selfInputCh 通知主 Agent 查看结果
|
||||
func (a *Agent) executeSpawnChild(tc agentAPI.ToolCall) string {
|
||||
@ -2560,7 +2728,6 @@ func (a *Agent) runChildTask(taskID, task string) {
|
||||
// 子 Agent 可调用核心以外的全部工具(记忆/知识/文档/社交),但不能调用输出工具
|
||||
allTools := a.buildToolDefs()
|
||||
childTools := make([]interface{}, 0, len(allTools))
|
||||
outputTools := map[string]bool{"output_send": true, "output_list_channels": true, "spawn_child": true, "plgreload": true}
|
||||
for _, t := range allTools {
|
||||
toolMap, ok := t.(map[string]interface{})
|
||||
if !ok {
|
||||
@ -2571,9 +2738,10 @@ func (a *Agent) runChildTask(taskID, task string) {
|
||||
continue
|
||||
}
|
||||
name, _ := fn["name"].(string)
|
||||
if !outputTools[name] {
|
||||
childTools = append(childTools, t)
|
||||
if strings.HasPrefix(name, "output_send__") || name == "output_list_channels" || name == "spawn_child" || name == "plgreload" {
|
||||
continue
|
||||
}
|
||||
childTools = append(childTools, t)
|
||||
}
|
||||
|
||||
var finalResult string
|
||||
@ -2604,7 +2772,7 @@ func (a *Agent) runChildTask(taskID, task string) {
|
||||
for _, ct := range resp.ToolCalls {
|
||||
var result string
|
||||
switch {
|
||||
case ct.Name == "output_send" || ct.Name == "output_list_channels":
|
||||
case strings.HasPrefix(ct.Name, "output_send__") || ct.Name == "output_list_channels":
|
||||
result = fmt.Sprintf("子 Agent 不允许调用输出工具: %s", ct.Name)
|
||||
case ct.Name == "spawn_child" || ct.Name == "plgreload":
|
||||
result = fmt.Sprintf("子 Agent 不允许调用系统工具: %s", ct.Name)
|
||||
@ -2796,7 +2964,14 @@ func (a *Agent) runStage(stage sdk.Stage, ctx *sdk.StageContext) bool {
|
||||
return false
|
||||
}
|
||||
ctx.Phase = stage
|
||||
a.stageHost.RunStage(stage, ctx)
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[agent] stage %q plugin panic: %v\n%s", stage, r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
a.stageHost.RunStage(stage, ctx)
|
||||
}()
|
||||
return ctx.Response != nil
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
)
|
||||
|
||||
// mockOutputDevice implements agentIO.Device for testing output tools
|
||||
type mockOutputDevice struct {
|
||||
name string
|
||||
caps agentIO.OutputCapability
|
||||
@ -61,24 +61,11 @@ func TestExecuteOutputSendTool(t *testing.T) {
|
||||
})
|
||||
|
||||
a := &Agent{io: io}
|
||||
tc := agentAPI.ToolCall{Name: "output_send", Arguments: map[string]interface{}{
|
||||
"channel": "screen",
|
||||
"content": "hello world",
|
||||
tc := agentAPI.ToolCall{Name: "output_send__screen", Arguments: map[string]interface{}{
|
||||
"content": `{"content":"hello world"}`,
|
||||
}}
|
||||
result := a.executeOutputSendTool(tc)
|
||||
if result != "已通过 [screen] 通道发送" {
|
||||
t.Errorf("unexpected result: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteOutputSendToolMissingChannel(t *testing.T) {
|
||||
io := agentIO.NewIOManager()
|
||||
a := &Agent{io: io}
|
||||
tc := agentAPI.ToolCall{Name: "output_send", Arguments: map[string]interface{}{
|
||||
"content": "hello",
|
||||
}}
|
||||
result := a.executeOutputSendTool(tc)
|
||||
if result != "channel 和 content 不能为空" {
|
||||
if !strings.Contains(result, "screen") {
|
||||
t.Errorf("unexpected result: %s", result)
|
||||
}
|
||||
}
|
||||
@ -86,25 +73,22 @@ func TestExecuteOutputSendToolMissingChannel(t *testing.T) {
|
||||
func TestExecuteOutputSendToolEmptyContent(t *testing.T) {
|
||||
io := agentIO.NewIOManager()
|
||||
a := &Agent{io: io}
|
||||
tc := agentAPI.ToolCall{Name: "output_send", Arguments: map[string]interface{}{
|
||||
"channel": "screen",
|
||||
}}
|
||||
tc := agentAPI.ToolCall{Name: "output_send__screen", Arguments: map[string]interface{}{}}
|
||||
result := a.executeOutputSendTool(tc)
|
||||
if result != "channel 和 content 不能为空" {
|
||||
t.Errorf("unexpected result: %s", result)
|
||||
if result == "" || strings.Contains(result, "已通过") {
|
||||
t.Errorf("expected error for empty content, got: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteOutputSendToolChannelNotExist(t *testing.T) {
|
||||
io := agentIO.NewIOManager()
|
||||
a := &Agent{io: io}
|
||||
tc := agentAPI.ToolCall{Name: "output_send", Arguments: map[string]interface{}{
|
||||
"channel": "nonexistent",
|
||||
tc := agentAPI.ToolCall{Name: "output_send__nonexistent", Arguments: map[string]interface{}{
|
||||
"content": "hello",
|
||||
}}
|
||||
result := a.executeOutputSendTool(tc)
|
||||
if result != "通道 [nonexistent] 不存在或不可用。可用通道请用 output_list_channels 查看" {
|
||||
t.Errorf("unexpected result: %s", result)
|
||||
if !strings.Contains(result, "不存在") && !strings.Contains(result, "不可用") {
|
||||
t.Errorf("expected error for nonexistent channel, got: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,23 +100,26 @@ func TestExecuteOutputSendToolNoTextCap(t *testing.T) {
|
||||
})
|
||||
|
||||
a := &Agent{io: io}
|
||||
tc := agentAPI.ToolCall{Name: "output_send", Arguments: map[string]interface{}{
|
||||
"channel": "camera",
|
||||
tc := agentAPI.ToolCall{Name: "output_send__camera", Arguments: map[string]interface{}{
|
||||
"content": "hello",
|
||||
}}
|
||||
result := a.executeOutputSendTool(tc)
|
||||
if result == "已通过 [camera] 通道发送" {
|
||||
if strings.Contains(result, "已通过") {
|
||||
t.Errorf("should reject channel without text capability")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildToolDefsOutputToolsAlwaysPresent(t *testing.T) {
|
||||
func TestBuildToolDefsOutputToolsWithDevice(t *testing.T) {
|
||||
io := agentIO.NewIOManager()
|
||||
io.RegisterDevice(&mockOutputDevice{
|
||||
name: "screen",
|
||||
caps: agentIO.CapText,
|
||||
})
|
||||
a := &Agent{io: io, knowledge: nil, docStore: nil, pluginReg: nil}
|
||||
tools := a.buildToolDefs()
|
||||
|
||||
foundSend := false
|
||||
foundList := false
|
||||
foundHelp := false
|
||||
for _, td := range tools {
|
||||
m, ok := td.(map[string]interface{})
|
||||
if !ok {
|
||||
@ -144,17 +131,17 @@ func TestBuildToolDefsOutputToolsAlwaysPresent(t *testing.T) {
|
||||
}
|
||||
name, _ := fn["name"].(string)
|
||||
switch name {
|
||||
case "output_send":
|
||||
case "output_send__screen":
|
||||
foundSend = true
|
||||
case "output_list_channels":
|
||||
foundList = true
|
||||
case "output_send__screen_help":
|
||||
foundHelp = true
|
||||
}
|
||||
}
|
||||
if !foundSend {
|
||||
t.Error("output_send should always be in tools")
|
||||
t.Error("output_send__screen should be in tools when device registered")
|
||||
}
|
||||
if !foundList {
|
||||
t.Error("output_list_channels should always be in tools")
|
||||
if !foundHelp {
|
||||
t.Error("output_send__screen_help should be in tools when device registered")
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,8 +149,7 @@ func TestGetAllToolsEmpty(t *testing.T) {
|
||||
io := agentIO.NewIOManager()
|
||||
a := &Agent{io: io}
|
||||
tools := a.buildToolDefs()
|
||||
// should have at least output_send, output_list_channels
|
||||
if len(tools) < 2 {
|
||||
t.Errorf("expected at least 2 tools, got %d", len(tools))
|
||||
if len(tools) < 1 {
|
||||
t.Errorf("expected at least 1 tool, got %d", len(tools))
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||||
)
|
||||
@ -28,18 +29,18 @@ const contextFlushInterval = 5 * time.Second
|
||||
|
||||
// RelevanceContext — 基于相关性的上下文管理,非固定阈值
|
||||
type RelevanceContext struct {
|
||||
mu sync.Mutex
|
||||
events []*ContextEvent
|
||||
veczer *vector.TFIDFVectorizer
|
||||
trained bool
|
||||
savePath string // 持久化路径,空则不持久化
|
||||
mu sync.Mutex
|
||||
events []*ContextEvent
|
||||
embedder *memory.LocalWordEmbedder
|
||||
trained bool
|
||||
savePath string
|
||||
saveTimer *time.Timer
|
||||
dirty bool
|
||||
dirty bool
|
||||
}
|
||||
|
||||
func NewRelevanceContext(savePath string) *RelevanceContext {
|
||||
rc := &RelevanceContext{
|
||||
veczer: vector.NewTFIDFVectorizer(2),
|
||||
embedder: memory.NewLocalWordEmbedder(),
|
||||
savePath: savePath,
|
||||
}
|
||||
if savePath != "" {
|
||||
@ -59,7 +60,7 @@ func (c *RelevanceContext) load() {
|
||||
return
|
||||
}
|
||||
for _, evt := range events {
|
||||
evt.Vector = c.veczer.Vectorize(evt.Input + " " + evt.Response)
|
||||
evt.Vector = c.embedder.Vectorize(evt.Input + " " + evt.Response)
|
||||
}
|
||||
c.events = events
|
||||
}
|
||||
@ -83,10 +84,9 @@ func (c *RelevanceContext) Append(evt ContextEvent) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
evt.Vector = c.veczer.Vectorize(evt.Input + " " + evt.Response)
|
||||
evt.Vector = c.embedder.Vectorize(evt.Input + " " + evt.Response)
|
||||
c.events = append(c.events, &evt)
|
||||
|
||||
// 增量训练向量化器
|
||||
c.trained = false
|
||||
|
||||
c.save()
|
||||
@ -146,10 +146,9 @@ func (c *RelevanceContext) Prune(currentInput string, topK int, docStore *docume
|
||||
return 0
|
||||
}
|
||||
|
||||
// 确保向量化器已训练
|
||||
c.ensureTrained()
|
||||
|
||||
queryVec := c.veczer.Vectorize(currentInput)
|
||||
queryVec := c.embedder.Vectorize(currentInput)
|
||||
|
||||
// 计算每条候选上下文与当前输入的相关性
|
||||
type scored struct {
|
||||
@ -263,10 +262,9 @@ func (c *RelevanceContext) ensureTrained() {
|
||||
for i, evt := range c.events {
|
||||
texts[i] = evt.Input + " " + evt.Response
|
||||
}
|
||||
c.veczer.Train(texts)
|
||||
// 重算所有事件向量,与新的向量化器特征空间对齐
|
||||
c.embedder.Train(texts)
|
||||
for _, evt := range c.events {
|
||||
evt.Vector = c.veczer.Vectorize(evt.Input + " " + evt.Response)
|
||||
evt.Vector = c.embedder.Vectorize(evt.Input + " " + evt.Response)
|
||||
}
|
||||
c.trained = true
|
||||
}
|
||||
|
||||
128
internal/agent/core/plugin_health.go
Normal file
128
internal/agent/core/plugin_health.go
Normal file
@ -0,0 +1,128 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
maxPluginCrashes = 3
|
||||
crashWindow = 5 * time.Minute
|
||||
reloadCooldown = 30 * time.Second
|
||||
)
|
||||
|
||||
type pluginHealthTracker struct {
|
||||
mu sync.Mutex
|
||||
records map[string]*pluginHealthRecord
|
||||
}
|
||||
|
||||
type pluginHealthRecord struct {
|
||||
CrashCount int
|
||||
FirstCrash time.Time
|
||||
LastCrash time.Time
|
||||
Unhealthy bool
|
||||
LastReload time.Time
|
||||
}
|
||||
|
||||
func newPluginHealthTracker() *pluginHealthTracker {
|
||||
return &pluginHealthTracker{
|
||||
records: make(map[string]*pluginHealthRecord),
|
||||
}
|
||||
}
|
||||
|
||||
// recordCrash 记录一次崩溃,返回 true 表示需要触发重载
|
||||
func (t *pluginHealthTracker) recordCrash(plugin string) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
r, ok := t.records[plugin]
|
||||
if !ok {
|
||||
r = &pluginHealthRecord{}
|
||||
t.records[plugin] = r
|
||||
}
|
||||
|
||||
if now.Sub(r.LastCrash) > crashWindow {
|
||||
r.CrashCount = 0
|
||||
r.FirstCrash = now
|
||||
}
|
||||
|
||||
r.CrashCount++
|
||||
r.LastCrash = now
|
||||
|
||||
if r.CrashCount >= maxPluginCrashes {
|
||||
r.Unhealthy = true
|
||||
log.Printf("[plugin] %s: %d crashes within %v, marking unhealthy", plugin, r.CrashCount, crashWindow)
|
||||
return true
|
||||
}
|
||||
|
||||
log.Printf("[plugin] %s: crash #%d", plugin, r.CrashCount)
|
||||
return false
|
||||
}
|
||||
|
||||
// isHealthy 检查插件是否健康;冷却期后自动恢复
|
||||
func (t *pluginHealthTracker) isHealthy(plugin string) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
r, ok := t.records[plugin]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if !r.Unhealthy {
|
||||
return true
|
||||
}
|
||||
if time.Since(r.LastReload) > reloadCooldown {
|
||||
r.Unhealthy = false
|
||||
r.CrashCount = 0
|
||||
log.Printf("[plugin] %s: cooldown passed, restored to healthy", plugin)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// markReloaded 标记插件已重载
|
||||
func (t *pluginHealthTracker) markReloaded(plugin string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
r, ok := t.records[plugin]
|
||||
if ok {
|
||||
r.Unhealthy = false
|
||||
r.CrashCount = 0
|
||||
r.LastReload = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// pendingReloads 返回已过冷却期、需要重载的插件列表
|
||||
func (t *pluginHealthTracker) pendingReloads() []string {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
var result []string
|
||||
now := time.Now()
|
||||
for name, r := range t.records {
|
||||
if !r.Unhealthy {
|
||||
continue
|
||||
}
|
||||
if now.Sub(r.LastReload) > reloadCooldown {
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// unhealthyPlugins 返回当前所有不健康的插件名
|
||||
func (t *pluginHealthTracker) unhealthyPlugins() []string {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
var result []string
|
||||
for name, r := range t.records {
|
||||
if r.Unhealthy {
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
@ -53,7 +54,13 @@ func (h *StageHost) GetToolDefs() []sdk.ToolDef {
|
||||
return defs
|
||||
}
|
||||
|
||||
func (h *StageHost) ExecuteTool(name string, args map[string]interface{}) (interface{}, error) {
|
||||
func (h *StageHost) ExecuteTool(name string, args map[string]interface{}) (ret interface{}, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[stage] tool %s handler panic: %v\n%s", name, r, debug.Stack())
|
||||
err = fmt.Errorf("tool %s handler panic: %v", name, r)
|
||||
}
|
||||
}()
|
||||
h.mu.RLock()
|
||||
handler, ok := h.tools[name]
|
||||
h.mu.RUnlock()
|
||||
@ -72,6 +79,22 @@ func (h *StageHost) ToolPlugin(name string) string {
|
||||
return h.toolPlugins[name]
|
||||
}
|
||||
|
||||
func (h *StageHost) UnregisterPluginTools(pluginName string) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
var keepDefs []sdk.ToolDef
|
||||
for _, def := range h.toolDefs {
|
||||
if def.Plugin == pluginName {
|
||||
delete(h.tools, def.Name)
|
||||
delete(h.toolPlugins, def.Name)
|
||||
} else {
|
||||
keepDefs = append(keepDefs, def)
|
||||
}
|
||||
}
|
||||
h.toolDefs = keepDefs
|
||||
}
|
||||
|
||||
func inferToolPlugin(name string) string {
|
||||
for i := 0; i < len(name); i++ {
|
||||
if name[i] == '_' {
|
||||
@ -100,6 +123,11 @@ func (h *StageHost) RunStage(stage sdk.Stage, ctx *sdk.StageContext) {
|
||||
wg.Add(1)
|
||||
go func(fn sdk.StageHandler) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[stage] handler panic: %v", r)
|
||||
}
|
||||
}()
|
||||
if err := fn(ctx); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package io
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@ -143,6 +144,12 @@ func (m *IOManager) RegisterDevice(dev Device) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IOManager) GetDevice(name string) Device {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.devices[name]
|
||||
}
|
||||
|
||||
func (m *IOManager) StartAll() error {
|
||||
m.mu.RLock()
|
||||
devices := make([]Device, 0, len(m.devices))
|
||||
@ -336,7 +343,7 @@ func (m *IOManager) GetAllTools() []ToolDef {
|
||||
return tools
|
||||
}
|
||||
|
||||
func (m *IOManager) ExecuteTool(name string, args map[string]interface{}) (interface{}, error) {
|
||||
func (m *IOManager) ExecuteTool(name string, args map[string]interface{}) (ret interface{}, err error) {
|
||||
m.mu.RLock()
|
||||
type nameDevice struct {
|
||||
name string
|
||||
@ -356,6 +363,12 @@ func (m *IOManager) ExecuteTool(name string, args map[string]interface{}) (inter
|
||||
if len(candidates) == 0 {
|
||||
return nil, fmt.Errorf("tool %s not found", name)
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[io] tool %s execute panic: %v\n%s", name, r, debug.Stack())
|
||||
err = fmt.Errorf("tool %s execute panic: %v", name, r)
|
||||
}
|
||||
}()
|
||||
return candidates[0].dev.Execute(name, args)
|
||||
}
|
||||
|
||||
|
||||
@ -350,8 +350,9 @@ func (r *ConfigRegistry) seedDBValues(dataDir string) {
|
||||
set("core.agent.workdir", "")
|
||||
set("core.agent.system_prompt", `你是 HomeAgent,一个持续运行的个人管家。
|
||||
你的回复默认发送到用户的输入来源,无需额外工具。
|
||||
如需异步发送消息到其他通道,使用 output_send。
|
||||
使用 output_list_channels 查看可用通道。
|
||||
输出回复请使用 output_send__{通道名} 工具,content 为 JSON 字符串。用 output_list_channels 查看可用通道。
|
||||
使用 output_send__{通道名}_help 查看每个通道的 JSON 格式说明。
|
||||
输出通道可多次调用,长消息应当分多次发出而不是一口气发完。
|
||||
|
||||
当用户上传图片或音频时,系统会自动附着媒体内容。如果模型不支持直接处理多媒体,请调用对应的媒体处理工具。
|
||||
|
||||
|
||||
@ -2,18 +2,21 @@ package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventRawInput EventType = "raw_input"
|
||||
EventAgentOutput EventType = "agent_output"
|
||||
EventToolCall EventType = "tool_call"
|
||||
EventReasoning EventType = "reasoning"
|
||||
EventSystem EventType = "system"
|
||||
EventAll EventType = "*"
|
||||
EventRawInput EventType = "raw_input"
|
||||
EventAgentOutput EventType = "agent_output"
|
||||
EventAgentLLMChain EventType = "agent_llm_chain"
|
||||
EventToolCall EventType = "tool_call"
|
||||
EventReasoning EventType = "reasoning"
|
||||
EventStage EventType = "stage"
|
||||
EventSystem EventType = "system"
|
||||
EventAll EventType = "*"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
@ -45,13 +48,22 @@ func (b *Bus) Publish(evt *Event) {
|
||||
b.mu.RUnlock()
|
||||
|
||||
for _, h := range allHandlers {
|
||||
h(evt)
|
||||
b.safeCall(h, evt)
|
||||
}
|
||||
for _, h := range typeHandlers {
|
||||
h(evt)
|
||||
b.safeCall(h, evt)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bus) safeCall(h Handler, evt *Event) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[bus] handler panic: %v", r)
|
||||
}
|
||||
}()
|
||||
h(evt)
|
||||
}
|
||||
|
||||
func (b *Bus) Subscribe(eventType EventType, handler Handler) func() {
|
||||
b.mu.Lock()
|
||||
b.subs[eventType] = append(b.subs[eventType], handler)
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||||
)
|
||||
|
||||
@ -165,7 +166,7 @@ func (s *Store) Add(name, content string) error {
|
||||
Content: content,
|
||||
Path: path,
|
||||
Category: sanitize(category),
|
||||
Tags: extractKeywords(name + " " + content),
|
||||
Tags: memory.ExtractKeywords(name + " " + content),
|
||||
UpdatedAt: now,
|
||||
}
|
||||
s.items[id] = k
|
||||
@ -381,7 +382,7 @@ func (s *Store) scanDir(category, dirName string) {
|
||||
Content: content,
|
||||
Path: contentPath,
|
||||
Category: category,
|
||||
Tags: extractKeywords(dirName + " " + content),
|
||||
Tags: memory.ExtractKeywords(dirName + " " + content),
|
||||
UpdatedAt: now,
|
||||
}
|
||||
s.items[name] = k
|
||||
@ -411,33 +412,4 @@ func sanitize(name string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func extractKeywords(text string) []string {
|
||||
stopWords := map[string]bool{
|
||||
"的": true, "了": true, "是": true, "在": true, "有": true,
|
||||
"和": true, "就": true, "不": true, "都": true,
|
||||
"一": true, "一个": true, "也": true, "很": true,
|
||||
"到": true, "说": true, "要": true, "去": true,
|
||||
"会": true, "着": true, "没有": true, "看": true, "好": true,
|
||||
"自己": true, "这": true, "他": true, "她": true, "它": true,
|
||||
"什么": true, "怎么": true, "为什么": true, "如何": true,
|
||||
"我们": true, "你们": true, "他们": true, "这个": true,
|
||||
"那个": true, "可以": true, "吗": true, "吧": true, "啊": true,
|
||||
}
|
||||
|
||||
var keywords []string
|
||||
runes := []rune(text)
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for i := 0; i < len(runes)-1; i++ {
|
||||
word := string(runes[i : i+2])
|
||||
if !stopWords[word] && len(strings.TrimSpace(word)) == len(word) && !seen[word] {
|
||||
seen[word] = true
|
||||
keywords = append(keywords, word)
|
||||
}
|
||||
}
|
||||
|
||||
if len(keywords) > 10 {
|
||||
keywords = keywords[:10]
|
||||
}
|
||||
return keywords
|
||||
}
|
||||
|
||||
118
internal/memory/cut.go
Normal file
118
internal/memory/cut.go
Normal file
@ -0,0 +1,118 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/yanyiwu/gojieba"
|
||||
)
|
||||
|
||||
var (
|
||||
jiebaOnce sync.Once
|
||||
jiebaInst *gojieba.Jieba
|
||||
)
|
||||
|
||||
func GetJieba() *gojieba.Jieba {
|
||||
jiebaOnce.Do(func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[jieba] init panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
d := jiebaDictDir()
|
||||
if d == "" {
|
||||
log.Printf("[jieba] no dictionary directory found, jieba disabled")
|
||||
return
|
||||
}
|
||||
jiebaInst = gojieba.NewJieba(
|
||||
filepath.Join(d, "jieba.dict.utf8"),
|
||||
filepath.Join(d, "hmm_model.utf8"),
|
||||
filepath.Join(d, "user.dict.utf8"),
|
||||
filepath.Join(d, "idf.utf8"),
|
||||
filepath.Join(d, "stop_words.utf8"),
|
||||
)
|
||||
})
|
||||
return jiebaInst
|
||||
}
|
||||
|
||||
func jiebaDictDir() string {
|
||||
candidates := []string{
|
||||
os.Getenv("GOMODCACHE"),
|
||||
os.Getenv("GOPATH"),
|
||||
filepath.Join(os.Getenv("HOME"), "go"),
|
||||
"/root/go",
|
||||
"/go",
|
||||
"/home/program/go",
|
||||
}
|
||||
for _, base := range candidates {
|
||||
if base == "" {
|
||||
continue
|
||||
}
|
||||
d := filepath.Join(base, "pkg", "mod", "github.com", "yanyiwu", "gojieba@v1.4.7", "deps", "cppjieba", "dict")
|
||||
if info, err := os.Stat(d); err == nil && info.IsDir() {
|
||||
return d
|
||||
}
|
||||
// also try without "pkg/mod" (in case GOPATH is already the mod cache)
|
||||
d2 := filepath.Join(base, "github.com", "yanyiwu", "gojieba@v1.4.7", "deps", "cppjieba", "dict")
|
||||
if info, err := os.Stat(d2); err == nil && info.IsDir() {
|
||||
return d2
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var stopWords = map[string]bool{
|
||||
"的": true, "了": true, "是": true, "在": true, "有": true,
|
||||
"和": true, "就": true, "不": true, "人": true, "都": true,
|
||||
"一": true, "一个": true, "上": true, "也": true, "很": true,
|
||||
"到": true, "说": true, "要": true, "去": true, "你": true,
|
||||
"会": true, "着": true, "没有": true, "看": true, "好": true,
|
||||
"自己": true, "这": true, "他": true, "她": true, "它": true,
|
||||
"我": true, "我们": true, "你们": true, "他们": true,
|
||||
"吗": true, "吧": true, "啊": true,
|
||||
"嗯": true, "哦": true, "哈": true, "呀": true, "嘛": true,
|
||||
"然后": true, "因为": true, "所以": true, "如果": true, "但是": true,
|
||||
"可能": true, "还是": true, "已经": true,
|
||||
"就是": true, "不是": true, "是的": true,
|
||||
"非常": true, "比较": true, "应该": true, "需要": true,
|
||||
"能够": true, "目前": true, "现在": true, "今天": true, "昨天": true,
|
||||
"明天": true, "知道": true, "觉得": true, "认为": true,
|
||||
"能": true, "没": true, "对": true,
|
||||
"the": true, "a": true, "an": true, "is": true, "are": true,
|
||||
"was": true, "were": true, "be": true, "been": true, "being": true,
|
||||
"have": true, "has": true, "had": true, "do": true, "does": true,
|
||||
"did": true, "will": true, "would": true, "could": true, "should": true,
|
||||
"may": true, "might": true, "can": true, "shall": true, "this": true,
|
||||
"that": true, "these": true, "those": true, "it": true, "its": true,
|
||||
"and": true, "or": true, "but": true, "in": true, "on": true,
|
||||
"at": true, "to": true, "for": true, "of": true, "with": true,
|
||||
"what": true, "how": true, "why": true, "which": true, "where": true,
|
||||
"when": true, "who": true, "whom": true,
|
||||
}
|
||||
|
||||
func ExtractKeywords(text string) []string {
|
||||
x := GetJieba()
|
||||
if x == nil {
|
||||
return nil
|
||||
}
|
||||
words := x.Cut(text, true)
|
||||
var keywords []string
|
||||
seen := make(map[string]bool)
|
||||
for _, w := range words {
|
||||
if stopWords[w] || seen[w] {
|
||||
continue
|
||||
}
|
||||
r := []rune(w)
|
||||
if len(r) < 2 {
|
||||
continue
|
||||
}
|
||||
seen[w] = true
|
||||
keywords = append(keywords, w)
|
||||
}
|
||||
if len(keywords) > 5 {
|
||||
keywords = keywords[:5]
|
||||
}
|
||||
return keywords
|
||||
}
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||||
)
|
||||
|
||||
@ -388,7 +389,7 @@ func summarizeEntries(entries []ContextEntry) string {
|
||||
var topics []string
|
||||
for _, e := range entries {
|
||||
sources[e.Source]++
|
||||
words := extractKeywords(e.Content)
|
||||
words := memory.ExtractKeywords(e.Content)
|
||||
topics = append(topics, words...)
|
||||
}
|
||||
|
||||
@ -420,7 +421,7 @@ func summarizeEntries(entries []ContextEntry) string {
|
||||
func extractTags(entries []ContextEntry) []string {
|
||||
tagSet := make(map[string]bool)
|
||||
for _, e := range entries {
|
||||
for _, kw := range extractKeywords(e.Content) {
|
||||
for _, kw := range memory.ExtractKeywords(e.Content) {
|
||||
tagSet[kw] = true
|
||||
}
|
||||
}
|
||||
@ -439,7 +440,7 @@ func extractEntities(entries []ContextEntry) []string {
|
||||
var entities []string
|
||||
seen := make(map[string]bool)
|
||||
for _, e := range entries {
|
||||
for _, kw := range extractKeywords(e.Content) {
|
||||
for _, kw := range memory.ExtractKeywords(e.Content) {
|
||||
if len(kw) >= 2 && !seen[kw] {
|
||||
seen[kw] = true
|
||||
entities = append(entities, kw)
|
||||
@ -452,32 +453,6 @@ func extractEntities(entries []ContextEntry) []string {
|
||||
return entities
|
||||
}
|
||||
|
||||
func extractKeywords(text string) []string {
|
||||
stopWords := map[string]bool{
|
||||
"的": true, "了": true, "是": true, "在": true, "有": true,
|
||||
"和": true, "就": true, "不": true, "人": true, "都": true,
|
||||
"一": true, "一个": true, "上": true, "也": true, "很": true,
|
||||
"到": true, "说": true, "要": true, "去": true, "你": true,
|
||||
"会": true, "着": true, "没有": true, "看": true, "好": true,
|
||||
"自己": true, "这": true, "他": true, "她": true, "它": true,
|
||||
"什么": true, "怎么": true, "为什么": true, "如何": true,
|
||||
"我": true, "我们": true, "你们": true, "他们": true, "这个": true,
|
||||
"那个": true, "可以": true, "吗": true, "吧": true, "啊": true,
|
||||
}
|
||||
|
||||
var keywords []string
|
||||
runes := []rune(text)
|
||||
|
||||
// bi-gram
|
||||
for i := 0; i < len(runes)-1; i++ {
|
||||
word := string(runes[i : i+2])
|
||||
if !stopWords[word] && len(strings.TrimSpace(word)) == len(word) {
|
||||
keywords = append(keywords, word)
|
||||
}
|
||||
}
|
||||
return keywords
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
runes := []rune(s)
|
||||
if len(runes) > max {
|
||||
|
||||
@ -4,6 +4,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
)
|
||||
|
||||
func TestInsertAndQuery(t *testing.T) {
|
||||
@ -186,7 +188,7 @@ func TestSummarizeEntries(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExtractKeywords(t *testing.T) {
|
||||
kws := extractKeywords("今天天气很好")
|
||||
kws := memory.ExtractKeywords("今天天气很好")
|
||||
if len(kws) == 0 {
|
||||
t.Error("should extract keywords from Chinese text")
|
||||
}
|
||||
|
||||
221
internal/memory/embedder.go
Normal file
221
internal/memory/embedder.go
Normal file
@ -0,0 +1,221 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/yanyiwu/gojieba"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||||
)
|
||||
|
||||
type LocalWordEmbedder struct {
|
||||
mu sync.RWMutex
|
||||
jieba *gojieba.Jieba
|
||||
stopWords map[string]bool
|
||||
|
||||
docFreq map[string]float64
|
||||
totalDocs int
|
||||
|
||||
coOccur map[string]map[string]float64
|
||||
|
||||
vocab map[string]bool
|
||||
trained bool
|
||||
}
|
||||
|
||||
func NewLocalWordEmbedder() *LocalWordEmbedder {
|
||||
sw := make(map[string]bool)
|
||||
for k, v := range stopWords {
|
||||
sw[k] = v
|
||||
}
|
||||
return &LocalWordEmbedder{
|
||||
jieba: GetJieba(),
|
||||
stopWords: sw,
|
||||
docFreq: make(map[string]float64),
|
||||
coOccur: make(map[string]map[string]float64),
|
||||
vocab: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *LocalWordEmbedder) tokenize(text string) []string {
|
||||
if e.jieba == nil {
|
||||
return nil
|
||||
}
|
||||
words := e.jieba.Cut(text, true)
|
||||
var result []string
|
||||
seen := make(map[string]bool)
|
||||
for _, w := range words {
|
||||
w = strings.TrimSpace(w)
|
||||
if w == "" || e.stopWords[w] || seen[w] {
|
||||
continue
|
||||
}
|
||||
runes := []rune(w)
|
||||
if len(runes) < 2 {
|
||||
continue
|
||||
}
|
||||
seen[w] = true
|
||||
result = append(result, w)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (e *LocalWordEmbedder) Train(docs []string) {
|
||||
if e.jieba == nil {
|
||||
return
|
||||
}
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.docFreq = make(map[string]float64)
|
||||
e.coOccur = make(map[string]map[string]float64)
|
||||
e.vocab = make(map[string]bool)
|
||||
|
||||
tokenized := make([][]string, len(docs))
|
||||
|
||||
for i, doc := range docs {
|
||||
tokens := e.tokenize(doc)
|
||||
tokenized[i] = tokens
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for _, t := range tokens {
|
||||
e.vocab[t] = true
|
||||
if !seen[t] {
|
||||
e.docFreq[t]++
|
||||
seen[t] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
e.totalDocs = len(docs)
|
||||
|
||||
windowSize := 5
|
||||
for _, tokens := range tokenized {
|
||||
for i, word := range tokens {
|
||||
start := i - windowSize
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := i + windowSize + 1
|
||||
if end > len(tokens) {
|
||||
end = len(tokens)
|
||||
}
|
||||
for j := start; j < end; j++ {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
ctx := tokens[j]
|
||||
if e.coOccur[word] == nil {
|
||||
e.coOccur[word] = make(map[string]float64)
|
||||
}
|
||||
e.coOccur[word][ctx]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for word, ctxs := range e.coOccur {
|
||||
totalPairs := 0.0
|
||||
for _, count := range ctxs {
|
||||
totalPairs += count
|
||||
}
|
||||
pWord := e.docFreq[word] / float64(e.totalDocs)
|
||||
for ctx, count := range ctxs {
|
||||
pCtx := e.docFreq[ctx] / float64(e.totalDocs)
|
||||
pJoint := count / totalPairs
|
||||
pmi := math.Log2(pJoint / (pWord * pCtx))
|
||||
if pmi <= 0 {
|
||||
delete(ctxs, ctx)
|
||||
} else {
|
||||
ctxs[ctx] = pmi
|
||||
}
|
||||
}
|
||||
e.coOccur[word] = pruneTopK(ctxs, 50)
|
||||
}
|
||||
|
||||
e.trained = true
|
||||
}
|
||||
|
||||
func pruneTopK(m map[string]float64, k int) map[string]float64 {
|
||||
if len(m) <= k {
|
||||
return m
|
||||
}
|
||||
type kv struct {
|
||||
k string
|
||||
v float64
|
||||
}
|
||||
var sorted []kv
|
||||
for key, val := range m {
|
||||
sorted = append(sorted, kv{key, val})
|
||||
}
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].v > sorted[j].v
|
||||
})
|
||||
result := make(map[string]float64, k)
|
||||
for i := 0; i < k; i++ {
|
||||
result[sorted[i].k] = sorted[i].v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (e *LocalWordEmbedder) Vectorize(text string) vector.Vector {
|
||||
e.mu.RLock()
|
||||
useEmbedding := e.trained
|
||||
e.mu.RUnlock()
|
||||
|
||||
tokens := e.tokenize(text)
|
||||
if len(tokens) == 0 {
|
||||
return vector.Vector{}
|
||||
}
|
||||
|
||||
tf := make(map[string]float64)
|
||||
for _, t := range tokens {
|
||||
tf[t]++
|
||||
}
|
||||
maxTF := 0.0
|
||||
for _, count := range tf {
|
||||
if count > maxTF {
|
||||
maxTF = count
|
||||
}
|
||||
}
|
||||
|
||||
vec := make(vector.Vector)
|
||||
|
||||
if useEmbedding {
|
||||
e.mu.RLock()
|
||||
for word, count := range tf {
|
||||
tfidf := (count / maxTF) * idf(e.docFreq[word], e.totalDocs)
|
||||
|
||||
if ctxs, ok := e.coOccur[word]; ok {
|
||||
for ctx, pmi := range ctxs {
|
||||
vec[ctx] += tfidf * pmi
|
||||
}
|
||||
}
|
||||
|
||||
vec["__w__"+word] += tfidf
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
} else {
|
||||
for word, count := range tf {
|
||||
tfNorm := count / maxTF
|
||||
var df float64
|
||||
e.mu.RLock()
|
||||
df = e.docFreq[word]
|
||||
e.mu.RUnlock()
|
||||
vec[word] = tfNorm * idf(df, e.totalDocs)
|
||||
}
|
||||
}
|
||||
|
||||
return vec
|
||||
}
|
||||
|
||||
func idf(df float64, total int) float64 {
|
||||
if df <= 0 || total <= 0 {
|
||||
return 1.0
|
||||
}
|
||||
return math.Log(float64(total+1)/(df+1)+1) + 1
|
||||
}
|
||||
|
||||
func (e *LocalWordEmbedder) Trained() bool {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
return e.trained
|
||||
}
|
||||
@ -132,6 +132,9 @@ func (g *GraphDB) Commit(triples []Triple, sessionID string, turnID int) (int, i
|
||||
if t.Subject == "" || t.Relation == "" || t.Object == "" {
|
||||
continue
|
||||
}
|
||||
if !validEntityName(t.Subject) || !validEntityName(t.Object) {
|
||||
continue
|
||||
}
|
||||
|
||||
subjType := t.SubjectType
|
||||
if subjType == "" {
|
||||
@ -161,11 +164,11 @@ func (g *GraphDB) Commit(triples []Triple, sessionID string, turnID int) (int, i
|
||||
var sourceID, targetID int64
|
||||
err = tx.QueryRow("SELECT id FROM entities WHERE name = ?", t.Subject).Scan(&sourceID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
return 0, 0, fmt.Errorf("subject %q: %w", t.Subject, err)
|
||||
}
|
||||
err = tx.QueryRow("SELECT id FROM entities WHERE name = ?", t.Object).Scan(&targetID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
return 0, 0, fmt.Errorf("object %q: %w", t.Object, err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(
|
||||
@ -186,7 +189,27 @@ func (g *GraphDB) Commit(triples []Triple, sessionID string, turnID int) (int, i
|
||||
return entitiesCreated, relationsCreated, nil
|
||||
}
|
||||
|
||||
func validEntityName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
r := []rune(name)
|
||||
if len(r) < 2 || len(r) > 50 {
|
||||
return false
|
||||
}
|
||||
hasLetter := false
|
||||
for _, ch := range r {
|
||||
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '\u4e00' && ch <= '\u9fff') || ch == '-' || ch == '_' {
|
||||
hasLetter = true
|
||||
}
|
||||
}
|
||||
return hasLetter
|
||||
}
|
||||
|
||||
func (g *GraphDB) upsertEntity(tx *sql.Tx, name string, entityType string) (int, error) {
|
||||
if !validEntityName(name) {
|
||||
return 0, nil
|
||||
}
|
||||
result, err := tx.Exec(
|
||||
`INSERT INTO entities (name, type) VALUES (?, ?)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
|
||||
@ -94,7 +94,7 @@ func (idx *Indexer) BuildContext(userInput string) *InjectedContext {
|
||||
vectorEntities := idx.vectorSearchEntities(userInput)
|
||||
|
||||
// 2. 关键词搜索:已有逻辑
|
||||
keywords := extractKeywords(userInput)
|
||||
keywords := ExtractKeywords(userInput)
|
||||
if len(keywords) == 0 && len(vectorEntities) == 0 {
|
||||
keywords = []string{userInput}
|
||||
}
|
||||
@ -282,46 +282,6 @@ func (idx *Indexer) GetToolDefinitions() []map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func extractKeywords(input string) []string {
|
||||
stopWords := map[string]bool{
|
||||
"的": true, "了": true, "是": true, "在": true, "有": true,
|
||||
"和": true, "就": true, "不": true, "人": true, "都": true,
|
||||
"一": true, "一个": true, "上": true, "也": true, "很": true,
|
||||
"到": true, "说": true, "要": true, "去": true, "你": true,
|
||||
"会": true, "着": true, "没有": true, "看": true, "好": true,
|
||||
"自己": true, "这": true, "他": true, "她": true, "它": true,
|
||||
"什么": true, "怎么": true, "为什么": true, "如何": true,
|
||||
}
|
||||
|
||||
var keywords []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
runes := []rune(input)
|
||||
|
||||
bigram := []rune{}
|
||||
for _, r := range runes {
|
||||
bigram = append(bigram, r)
|
||||
if len(bigram) >= 2 {
|
||||
word := string(bigram)
|
||||
if !stopWords[word] && !seen[word] {
|
||||
seen[word] = true
|
||||
keywords = append(keywords, word)
|
||||
}
|
||||
bigram = bigram[1:]
|
||||
}
|
||||
}
|
||||
|
||||
if len(keywords) == 0 && len(runes) > 0 {
|
||||
keywords = []string{string(runes)}
|
||||
}
|
||||
|
||||
if len(keywords) > 5 {
|
||||
keywords = keywords[:5]
|
||||
}
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
func buildIndexSummary(entities []Entity) string {
|
||||
if len(entities) == 0 {
|
||||
return ""
|
||||
|
||||
@ -186,9 +186,9 @@ func TestExtractKeywords(t *testing.T) {
|
||||
{"的了的", 0}, // all stop words
|
||||
}
|
||||
for _, tt := range tests {
|
||||
kw := extractKeywords(tt.input)
|
||||
kw := ExtractKeywords(tt.input)
|
||||
if len(kw) < tt.min {
|
||||
t.Errorf("extractKeywords(%q) = %v, want at least %d keywords", tt.input, kw, tt.min)
|
||||
t.Errorf("ExtractKeywords(%q) = %v, want at least %d keywords", tt.input, kw, tt.min)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ func TestBridgeE2E_WebPlugin(t *testing.T) {
|
||||
}
|
||||
|
||||
sett := sdk.NewSettings("web", nil)
|
||||
psdk := sdk.New("web", nil, nil, nil, nil, nil, nil, nil, sett, regTool, regStage, regAPI)
|
||||
psdk := sdk.New("web", sdk.SDKConfig{Settings: sett, RegTool: regTool, RegStage: regStage, RegAPI: regAPI})
|
||||
|
||||
plg, err := newDLLPlugin(dllPath, "web", nil)
|
||||
if err != nil {
|
||||
@ -136,11 +136,11 @@ func TestBridgeE2E_SanitizerStages(t *testing.T) {
|
||||
}
|
||||
|
||||
sett := sdk.NewSettings("sanitizer", nil)
|
||||
psdk := sdk.New("sanitizer", nil, nil, nil, nil, nil, nil, nil, sett,
|
||||
func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error { return nil },
|
||||
regStage,
|
||||
func(name string) error { return nil },
|
||||
)
|
||||
psdk := sdk.New("sanitizer", sdk.SDKConfig{Settings: sett,
|
||||
RegTool: func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error { return nil },
|
||||
RegStage: regStage,
|
||||
RegAPI: func(name string) error { return nil },
|
||||
})
|
||||
|
||||
plg, err := newDLLPlugin(dllPath, "sanitizer", nil)
|
||||
if err != nil {
|
||||
|
||||
@ -60,6 +60,8 @@ type Registry struct {
|
||||
instances []sdk.Plugin
|
||||
factories map[string]NativeFactory
|
||||
|
||||
pluginAutoRestart map[string]bool
|
||||
|
||||
iom *agentIO.IOManager
|
||||
evBus *events.Bus
|
||||
memDB *memory.GraphDB
|
||||
@ -77,8 +79,9 @@ type Registry struct {
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
plugins: make(map[string]sdk.Plugin),
|
||||
factories: make(map[string]NativeFactory),
|
||||
plugins: make(map[string]sdk.Plugin),
|
||||
factories: make(map[string]NativeFactory),
|
||||
pluginAutoRestart: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +105,24 @@ func (r *Registry) RegisterNative(name string, factory NativeFactory) {
|
||||
globalFactories.Store(name, factory)
|
||||
}
|
||||
|
||||
type channelDevice struct {
|
||||
name string
|
||||
desc string
|
||||
caps agentIO.OutputCapability
|
||||
handler sdk.ToolHandler
|
||||
}
|
||||
|
||||
func (d *channelDevice) Name() string { return d.name }
|
||||
func (d *channelDevice) Type() agentIO.DeviceType { return agentIO.DeviceOutput }
|
||||
func (d *channelDevice) Description() string { return d.desc }
|
||||
func (d *channelDevice) Start() error { return nil }
|
||||
func (d *channelDevice) Stop() error { return nil }
|
||||
func (d *channelDevice) OutputCapabilities() agentIO.OutputCapability { return d.caps }
|
||||
func (d *channelDevice) Tools() []agentIO.ToolDef { return nil }
|
||||
func (d *channelDevice) Execute(tool string, args map[string]interface{}) (interface{}, error) {
|
||||
return d.handler(args)
|
||||
}
|
||||
|
||||
func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
|
||||
sett := sdk.NewSettings(name, r.cfgReg)
|
||||
|
||||
@ -120,16 +141,32 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
|
||||
regAPI = func(name string) error { return nil }
|
||||
}
|
||||
|
||||
return sdk.New(name,
|
||||
r.iom, r.evBus,
|
||||
sdk.NewGraphMemory(r.memDB),
|
||||
sdk.NewTextMemory(r.textMem),
|
||||
sdk.NewDocMemory(r.docStore),
|
||||
sdk.NewKnowledge(r.ks),
|
||||
sdk.NewLLM(r.mgr),
|
||||
sett,
|
||||
regTool, regStage, regAPI,
|
||||
)
|
||||
regOutput := func(chName string, caps int, desc string, handler sdk.ToolHandler) error {
|
||||
if r.iom == nil {
|
||||
return nil
|
||||
}
|
||||
return r.iom.RegisterDevice(&channelDevice{
|
||||
name: chName,
|
||||
caps: agentIO.OutputCapability(caps),
|
||||
desc: desc,
|
||||
handler: handler,
|
||||
})
|
||||
}
|
||||
|
||||
return sdk.New(name, sdk.SDKConfig{
|
||||
IOManager: r.iom,
|
||||
EventBus: r.evBus,
|
||||
Memory: sdk.NewGraphMemory(r.memDB),
|
||||
TextMemory: sdk.NewTextMemory(r.textMem),
|
||||
DocMemory: sdk.NewDocMemory(r.docStore),
|
||||
Knowledge: sdk.NewKnowledge(r.ks),
|
||||
LLM: sdk.NewLLM(r.mgr),
|
||||
Settings: sett,
|
||||
RegTool: regTool,
|
||||
RegStage: regStage,
|
||||
RegAPI: regAPI,
|
||||
RegOutput: regOutput,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Registry) Load(dir string) error {
|
||||
@ -201,6 +238,7 @@ func (r *Registry) Load(dir string) error {
|
||||
|
||||
r.mu.Lock()
|
||||
r.plugins[name] = p
|
||||
r.pluginAutoRestart[name] = plgSDK.AutoRestart()
|
||||
r.instances = append(r.instances, p)
|
||||
r.mu.Unlock()
|
||||
log.Printf("[plugin] loaded: %s", name)
|
||||
@ -263,6 +301,7 @@ func (r *Registry) loadOne(plgDir, name string) bool {
|
||||
|
||||
r.mu.Lock()
|
||||
r.plugins[name] = plg
|
||||
r.pluginAutoRestart[name] = plgSDK.AutoRestart()
|
||||
r.instances = append(r.instances, plg)
|
||||
r.mu.Unlock()
|
||||
log.Printf("[plugin] loaded: %s", name)
|
||||
@ -279,6 +318,7 @@ func (r *Registry) StopAll() {
|
||||
}
|
||||
r.plugins = make(map[string]sdk.Plugin)
|
||||
r.instances = nil
|
||||
r.pluginAutoRestart = make(map[string]bool)
|
||||
}
|
||||
|
||||
func (r *Registry) Reload(dir string) (string, error) {
|
||||
@ -289,6 +329,32 @@ func (r *Registry) Reload(dir string) (string, error) {
|
||||
return fmt.Sprintf("loaded %d plugins", len(r.instances)), nil
|
||||
}
|
||||
|
||||
func (r *Registry) ReloadOne(name string) error {
|
||||
plgDir := filepath.Join(r.plgDir, name)
|
||||
|
||||
r.mu.Lock()
|
||||
if p, ok := r.plugins[name]; ok {
|
||||
if err := p.Stop(); err != nil {
|
||||
log.Printf("[plugin] stop %s for reload: %v", name, err)
|
||||
}
|
||||
delete(r.plugins, name)
|
||||
for i, inst := range r.instances {
|
||||
if inst.Name() == name {
|
||||
r.instances = append(r.instances[:i], r.instances[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
ok := r.loadOne(plgDir, name)
|
||||
if !ok {
|
||||
return fmt.Errorf("reload plugin %s failed", name)
|
||||
}
|
||||
log.Printf("[plugin] reloaded: %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) List() []string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
@ -306,6 +372,16 @@ func (r *Registry) Get(name string) sdk.Plugin {
|
||||
return r.plugins[name]
|
||||
}
|
||||
|
||||
func (r *Registry) AutoRestartEnabled(name string) bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
enabled, ok := r.pluginAutoRestart[name]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
func (r *Registry) PluginMetas() map[string]PluginMeta {
|
||||
metas := make(map[string]PluginMeta)
|
||||
globalPluginMeta.Range(func(key, val interface{}) bool {
|
||||
|
||||
@ -177,6 +177,7 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "default_timeout", Type: "string", DisplayName: "默认终端超时",
|
||||
Description: "终端自动关闭的默认时间,例如 5m, 10m, 30m, 1h(默认 5m)",
|
||||
|
||||
@ -31,8 +31,7 @@ func (tc *toolCapture) RegisterAPI(name string) error {
|
||||
func setupPlugin() (*Plugin, *toolCapture, error) {
|
||||
p := New("agentcli")
|
||||
tc := newToolCapture()
|
||||
// Use nil for fields we don't need (iom, eventBus, etc.)
|
||||
sdk := sdk.New("agentcli", nil, nil, nil, nil, nil, nil, nil, nil, tc.RegisterTool, tc.RegisterStage, tc.RegisterAPI)
|
||||
sdk := sdk.New("agentcli", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
||||
if err := p.Start(sdk); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@ -70,6 +70,7 @@ func New(name, socketPath string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "api_key", Type: "password", DisplayName: "CLI API 密钥",
|
||||
Description: "CLI 客户端连接时需提供的认证密钥(留空则使用 WebUI 密钥)",
|
||||
|
||||
@ -81,6 +81,7 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "default_timeout", Type: "string", DisplayName: "默认命令超时",
|
||||
Description: "命令执行的默认超时时间,例如 30s, 1m, 5m(默认 30s)",
|
||||
|
||||
@ -34,7 +34,7 @@ func (tc *toolCapture) RegisterAPI(name string) error { return nil }
|
||||
func setupPlugin() (*Plugin, *toolCapture, error) {
|
||||
p := New("cmd")
|
||||
tc := newToolCapture()
|
||||
sdk := sdk.New("cmd", nil, nil, nil, nil, nil, nil, nil, nil, tc.RegisterTool, tc.RegisterStage, tc.RegisterAPI)
|
||||
sdk := sdk.New("cmd", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
||||
if err := p.Start(sdk); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
p.sdk = s
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "dir",
|
||||
|
||||
@ -113,6 +113,7 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
p.autoInterval = 30 * time.Minute
|
||||
p.llmTimeout = 120 * time.Second
|
||||
p.llmMaxTurns = 20
|
||||
|
||||
@ -42,7 +42,7 @@ func setupPlugin() (*Plugin, *toolCapture, error) {
|
||||
Configure(sh, iom, pr, nil, nil, nil, nil, nil)
|
||||
p := New("healthcheck")
|
||||
tc := newToolCapture()
|
||||
sdk := sdk.New("healthcheck", nil, nil, nil, nil, nil, nil, nil, nil, tc.RegisterTool, tc.RegisterStage, tc.RegisterAPI)
|
||||
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
||||
if err := p.Start(sdk); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@ -192,7 +192,7 @@ func TestHealthcheckWithMemory(t *testing.T) {
|
||||
Configure(sh, iom, pr, memDB, nil, nil, nil, nil)
|
||||
p := New("healthcheck")
|
||||
tc := newToolCapture()
|
||||
sdk := sdk.New("healthcheck", nil, nil, nil, nil, nil, nil, nil, nil, tc.RegisterTool, tc.RegisterStage, tc.RegisterAPI)
|
||||
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
||||
if err := p.Start(sdk); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -235,7 +235,7 @@ func TestHealthcheckWithKnowledge(t *testing.T) {
|
||||
Configure(sh, iom, pr, nil, ks, nil, nil, nil)
|
||||
p := New("healthcheck")
|
||||
tc := newToolCapture()
|
||||
sdk := sdk.New("healthcheck", nil, nil, nil, nil, nil, nil, nil, nil, tc.RegisterTool, tc.RegisterStage, tc.RegisterAPI)
|
||||
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
||||
if err := p.Start(sdk); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -289,7 +289,7 @@ func TestHealthcheckWithDocStore(t *testing.T) {
|
||||
Configure(sh, iom, pr, nil, nil, ds, nil, nil)
|
||||
p := New("healthcheck")
|
||||
tc := newToolCapture()
|
||||
sdk := sdk.New("healthcheck", nil, nil, nil, nil, nil, nil, nil, nil, tc.RegisterTool, tc.RegisterStage, tc.RegisterAPI)
|
||||
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
||||
if err := p.Start(sdk); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
cfgs, err := p.loadConfig(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load mcp config: %w", err)
|
||||
|
||||
@ -68,6 +68,7 @@ func New(name, skillsDir string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
p.sdk = s
|
||||
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
|
||||
@ -326,13 +326,13 @@ func TestLoadOCPluginViaPluginStart(t *testing.T) {
|
||||
|
||||
var registeredTools []string
|
||||
registeredHandlers := make(map[string]sdk.ToolHandler)
|
||||
sdk := sdk.New("openclaw", nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||
sdk := sdk.New("openclaw", sdk.SDKConfig{
|
||||
RegTool: func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||
registeredTools = append(registeredTools, name)
|
||||
registeredHandlers[name] = handler
|
||||
return nil
|
||||
},
|
||||
nil, nil)
|
||||
})
|
||||
|
||||
if err := p.Start(sdk); err != nil {
|
||||
t.Fatalf("start plugin: %v", err)
|
||||
|
||||
@ -63,6 +63,7 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "http_addr",
|
||||
Default: HTTPAddr,
|
||||
|
||||
@ -40,6 +40,7 @@ func New(name string) *Plugin {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
p.maxDur = 24 * time.Hour
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "max_duration", Type: "string", DisplayName: "最大定时时长",
|
||||
|
||||
@ -4,9 +4,10 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HomeAgent Dashboard</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js" onerror="window._THREE_FAILED=true"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js" onerror="window._THREE_FAILED=true"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js" onerror="console.warn('marked CDN failed')"></script>
|
||||
<script>setTimeout(function(){if(!window.THREE)window._THREE_FAILED=true},8000)</script>
|
||||
<style>
|
||||
:root {
|
||||
--bg-primary: #0f172a;
|
||||
@ -152,9 +153,9 @@ code { font-family:monospace; font-size:12px; color:var(--pre-color) }
|
||||
.empty-state { text-align:center; padding:40px 20px; color:var(--text-muted) }
|
||||
.empty-state p { font-size:14px; margin-bottom:8px }
|
||||
.empty-state .icon { font-size:36px; margin-bottom:12px; opacity:.5 }
|
||||
.chat-layout { display:flex; gap:16px; height:calc(100vh - 100px); min-height:60vh }
|
||||
.chat-main { flex:2; min-width:0; display:flex; flex-direction:column }
|
||||
.chat-main .card { flex:1; display:flex; flex-direction:column; margin-bottom:0 }
|
||||
.chat-layout { display:flex; gap:16px; height:calc(100vh - 100px); min-height:60vh; overflow:hidden }
|
||||
.chat-main { flex:2; min-width:0; min-height:0; display:flex; flex-direction:column }
|
||||
.chat-main .card { flex:1; display:flex; flex-direction:column; margin-bottom:0; min-height:0 }
|
||||
.chat-main .card h2 { flex-shrink:0 }
|
||||
.chat-messages { flex:1; overflow-y:auto; padding:12px; border:1px solid var(--border-color); border-radius:8px; background:var(--chat-bg); margin-bottom:0; display:flex; flex-direction:column; gap:4px; min-height:0 }
|
||||
.msg { display:flex; gap:8px; margin-bottom:2px; align-items:flex-start; max-width:85% }
|
||||
@ -229,6 +230,20 @@ code { font-family:monospace; font-size:12px; color:var(--pre-color) }
|
||||
#sm-container-chat { height:260px; background:var(--bg-input); border-radius:6px; border:1px solid var(--border-color); overflow:hidden; position:relative }
|
||||
#sm-container-chat canvas { display:block }
|
||||
|
||||
.toggle-row { margin-top:8px; display:flex; align-items:center; gap:12px }
|
||||
.toggle-row .label-text { color:#8888aa; font-size:12px }
|
||||
.toggle-switch { position:relative; width:36px; height:20px; cursor:pointer; flex-shrink:0 }
|
||||
.toggle-track { position:absolute; inset:0; background:rgba(60,60,80,0.8); border-radius:10px; transition:all 0.3s; border:1px solid rgba(100,100,255,0.2) }
|
||||
.toggle-track.on { background:rgba(68,136,255,0.5); border-color:#4488ff }
|
||||
.toggle-knob { position:absolute; width:16px; height:16px; left:2px; top:2px; background:#6666aa; border-radius:50%; transition:all 0.3s }
|
||||
.toggle-knob.on { left:18px; background:#4488ff }
|
||||
.toggle-btn { display:flex; align-items:center; gap:4px; padding:2px 8px; border-radius:4px; border:1px solid rgba(100,100,255,0.15); background:transparent; color:#8888aa; font-size:12px; font-family:inherit; cursor:pointer; transition:all 0.2s }
|
||||
.toggle-btn:hover { background:rgba(68,136,255,0.15); color:#fff }
|
||||
.toggle-btn.on { background:rgba(68,136,255,0.3); color:#4488ff; border-color:#4488ff }
|
||||
.label-text { color:#8888aa; font-size:12px }
|
||||
.loading-spinner { width:32px; height:32px; border:3px solid rgba(68,136,255,0.15); border-top:3px solid #4488ff; border-radius:50%; animation:spin 0.8s linear infinite }
|
||||
@keyframes spin { to { transform:rotate(360deg) } }
|
||||
|
||||
.sidebar-subnav { display:flex; gap:0; border-bottom:1px solid var(--border-color); margin-bottom:10px }
|
||||
.sidebar-subnav span { padding:6px 12px; font-size:12px; cursor:pointer; color:var(--text-muted); border-bottom:2px solid transparent; transition:all .15s }
|
||||
.sidebar-subnav span:hover { color:var(--text-primary) }
|
||||
@ -298,6 +313,7 @@ let state = {
|
||||
selectedSection: 'core',
|
||||
messages: [],
|
||||
chatLoading: false,
|
||||
chatStage: '',
|
||||
healthResult: null,
|
||||
starmapInit: false,
|
||||
starmapLoading: false,
|
||||
@ -412,7 +428,7 @@ function switchTab(n) {
|
||||
|
||||
// ===== Tab Render Dispatch =====
|
||||
async function renderAll() {
|
||||
try { var s = await api('/status'); state.status = s } catch(e) {}
|
||||
try { var s = await api('/status'); state.status = s; state.startedAt = s.startedAt ? new Date(s.startedAt).getTime() : null } catch(e) {}
|
||||
try { state.kernel = await api('/kernel') } catch(e) {}
|
||||
try {
|
||||
var s = await api('/settings');
|
||||
@ -434,6 +450,30 @@ async function renderAll() {
|
||||
applyI18n();
|
||||
}
|
||||
|
||||
function fmtUptime(ms) {
|
||||
var s = Math.floor(ms / 1000);
|
||||
if (s < 60) return s + 's';
|
||||
var m = Math.floor(s / 60); s = s % 60;
|
||||
if (m < 60) return m + 'm ' + s + 's';
|
||||
var h = Math.floor(m / 60); m = m % 60;
|
||||
return h + 'h ' + m + 'm ' + s + 's';
|
||||
}
|
||||
|
||||
var uptimeTick = null;
|
||||
function startUptimeTicker() {
|
||||
if (uptimeTick) clearInterval(uptimeTick);
|
||||
uptimeTick = setInterval(function() {
|
||||
var el = document.querySelector('#uptime-val');
|
||||
if (el && state.startedAt) {
|
||||
var now = Date.now();
|
||||
el.textContent = fmtUptime(now - state.startedAt);
|
||||
} else if (!state.startedAt) {
|
||||
var el2 = document.querySelector('#uptime-val');
|
||||
if (el2) el2.textContent = '-';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// ===== Overview =====
|
||||
function statCard(l, v) {
|
||||
return '<div class="card stat-card"><div class="stat-value">' + v + '</div><div class="stat-label">' + l + '</div></div>';
|
||||
@ -444,7 +484,7 @@ function renderOverview() {
|
||||
var k = state.kernel;
|
||||
var html = '<div class="grid-4">'
|
||||
+ statCard(__('运行状态','Status'), s.status || 'unknown', 'running')
|
||||
+ statCard(__('运行时间','Uptime'), s.uptime || '-', 'uptime')
|
||||
+ statCard(__('运行时间','Uptime'), '<span id="uptime-val">' + (state.startedAt ? fmtUptime(Date.now() - state.startedAt) : '-') + '</span>', 'uptime')
|
||||
+ statCard(__('插件','Plugins'), (k?.plugins || []).length || 0, 'plugin')
|
||||
+ statCard(__('版本','Version'), s.version || '0.1.0', 'version')
|
||||
+ '</div>';
|
||||
@ -471,59 +511,15 @@ function renderOverview() {
|
||||
}
|
||||
|
||||
// ===== Chat =====
|
||||
function renderChat() {
|
||||
var _chatLayoutBuilt = false;
|
||||
|
||||
function buildChatLayout() {
|
||||
var cont = document.getElementById('tab-chat');
|
||||
var msgs = state.messages;
|
||||
var k = state.kernel;
|
||||
var k = state.kernel || {};
|
||||
var html = '<div class="chat-layout"><div class="chat-main">';
|
||||
html += '<div class="card"><h2>' + __('对话','Chat') + '</h2><div class="chat-messages" id="chat-msgs">';
|
||||
if (msgs.length === 0) {
|
||||
html += '<div class="card"><h2>' + __('对话','Chat') + ' <span id="chat-stage" class="badge" style="font-size:10px;font-weight:400;display:' + (state.chatLoading ? 'inline' : 'none') + '">' + escHtml(state.chatStage || '') + '</span></h2><div class="chat-messages" id="chat-msgs">';
|
||||
if (state.messages.length === 0) {
|
||||
html += '<div class="empty-state" style="flex:1;display:flex;align-items:center;justify-content:center"><p>' + __('开始对话以测试 Agent 回复','Start a conversation to test Agent replies') + '</p></div>';
|
||||
} else {
|
||||
msgs.forEach(function(m, i) {
|
||||
var role = m.role || 'user';
|
||||
var c = m.content || '';
|
||||
// Format content based on role
|
||||
if (role === 'assistant') {
|
||||
if (typeof marked !== 'undefined') { c = marked.parse(c) } else { c = '<pre>' + escHtml(c) + '</pre>' }
|
||||
} else if (role === 'system') {
|
||||
c = escHtml(c);
|
||||
} else {
|
||||
c = escHtml(c);
|
||||
}
|
||||
// Reasoning section
|
||||
var rc = '';
|
||||
if (m.reasoning_content) {
|
||||
var rcBody = (typeof marked !== 'undefined' ? marked.parse(m.reasoning_content) : escHtml(m.reasoning_content));
|
||||
rc = '<div class="reasoning">'
|
||||
+ '<div class="reasoning-title" onclick="var n=this.nextElementSibling;n.style.display=n.style.display===\'none\'?\'block\':\'none\';this.textContent=this.textContent===\'' + __('收起思考','Collapse') + '\'?\'' + __('展开思考','Expand') + '\':\'' + __('收起思考','Collapse') + '\'">' + __('收起思考','Collapse') + '</div>'
|
||||
+ '<div class="reasoning-body" style="display:none">' + rcBody + '</div></div>';
|
||||
}
|
||||
// Tool calls section
|
||||
var tcs = '';
|
||||
if (m.tool_calls && m.tool_calls.length > 0) {
|
||||
m.tool_calls.forEach(function(tc) {
|
||||
var argsStr = typeof tc.args === 'object' ? JSON.stringify(tc.args, null, 1) : (tc.args || '');
|
||||
var resultStr = tc.result ? (typeof tc.result === 'object' ? JSON.stringify(tc.result, null, 1).substring(0, 200) : String(tc.result).substring(0, 200)) : '';
|
||||
var statusIcon = tc.status === 'denied' ? '⛔' : '🔧';
|
||||
tcs += '<div class="tool-call">'
|
||||
+ '<div><span class="tc-name">' + statusIcon + ' ' + escHtml(tc.tool || tc.name || '') + '</span></div>'
|
||||
+ (argsStr && argsStr !== '{}' ? '<div class="tc-args">' + escHtml(argsStr) + '</div>' : '')
|
||||
+ (resultStr ? '<div class="tc-result">→ ' + escHtml(resultStr) + '</div>' : '')
|
||||
+ '</div>';
|
||||
});
|
||||
}
|
||||
// Build bubble content
|
||||
var body = rc + tcs + '<div class="text">' + c + '</div>';
|
||||
if (role === 'system') {
|
||||
html += '<div class="msg msg-system"><div class="msg-bubble">' + body + '</div></div>';
|
||||
} else {
|
||||
html += '<div class="msg msg-' + role + '">'
|
||||
+ '<div class="msg-avatar">' + (role === 'user' ? 'U' : 'A') + '</div>'
|
||||
+ '<div class="msg-content"><div class="msg-bubble">' + body + '</div></div>'
|
||||
+ '</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
html += '</div>'
|
||||
+ '<div class="chat-input-row">'
|
||||
@ -532,7 +528,7 @@ function renderChat() {
|
||||
+ '</div></div>';
|
||||
html += '</div><div class="chat-sidebar">'
|
||||
+ '<div class="card" style="padding:12px"><h2 style="font-size:13px;margin-bottom:8px">' + __('星图','Star Map') + '</h2>'
|
||||
+ '<div id="sm-container-chat" style="height:160px"><p style="color:var(--text-muted);padding:12px;text-align:center;font-size:11px">' + __('加载中..','Loading..') + '</p></div></div>'
|
||||
+ '<div id="sm-container-chat" style="height:160px;display:flex;align-items:center;justify-content:center"><div class="loading-spinner"></div></div></div>'
|
||||
+ '<div class="card" style="padding:12px"><h2 style="font-size:13px;margin-bottom:8px">' + __('终端','Terminal') + ' <span id="term-count-badge" class="badge badge-blue">0</span></h2>'
|
||||
+ '<div id="term-list" style="max-height:160px;overflow-y:auto;font-size:11px"></div></div>'
|
||||
+ '<div class="card" style="padding:12px"><h2 style="font-size:13px;margin-bottom:8px">' + __('命令历史','Command History') + ' <span id="cmd-count-badge" class="badge badge-blue">0</span></h2>'
|
||||
@ -569,24 +565,86 @@ function renderChat() {
|
||||
+ '<button class="btn btn-primary btn-sm" onclick="createKnowledgeChat()">' + __('创建','Create') + '</button>'
|
||||
+ '</div></div></div></div></div>';
|
||||
cont.innerHTML = html;
|
||||
var el = document.getElementById('chat-msgs');
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
renderChatStarmap();
|
||||
renderTerminals();
|
||||
renderCmdHistory();
|
||||
_chatLayoutBuilt = true;
|
||||
}
|
||||
|
||||
function rerenderChat() { renderChat(); renderChatStarmap() }
|
||||
function renderChat() {
|
||||
if (!_chatLayoutBuilt) { buildChatLayout(); renderChatStarmap(); renderTerminals(); renderCmdHistory() }
|
||||
var msgsEl = document.getElementById('chat-msgs');
|
||||
if (!msgsEl) return;
|
||||
var msgs = state.messages;
|
||||
var html = '';
|
||||
if (msgs.length === 0) {
|
||||
html = '<div class="empty-state" style="flex:1;display:flex;align-items:center;justify-content:center"><p>' + __('开始对话以测试 Agent 回复','Start a conversation to test Agent replies') + '</p></div>';
|
||||
} else {
|
||||
msgs.forEach(function(m, i) {
|
||||
var role = m.role || 'user';
|
||||
var c = m.content || '';
|
||||
if (role === 'assistant') {
|
||||
if (typeof marked !== 'undefined') { c = marked.parse(c) } else { c = '<pre>' + escHtml(c) + '</pre>' }
|
||||
} else if (role === 'system') {
|
||||
c = escHtml(c);
|
||||
} else {
|
||||
c = escHtml(c);
|
||||
}
|
||||
var rc = '';
|
||||
if (m.reasoning_content) {
|
||||
var rcBody = (typeof marked !== 'undefined' ? marked.parse(m.reasoning_content) : escHtml(m.reasoning_content));
|
||||
rc = '<div class="reasoning">'
|
||||
+ '<div class="reasoning-title" onclick="var n=this.nextElementSibling;n.style.display=n.style.display===\'none\'?\'block\':\'none\';this.textContent=this.textContent===\'' + __('收起思考','Collapse') + '\'?\'' + __('展开思考','Expand') + '\':\'' + __('收起思考','Collapse') + '\'">' + __('收起思考','Collapse') + '</div>'
|
||||
+ '<div class="reasoning-body" style="display:none">' + rcBody + '</div></div>';
|
||||
}
|
||||
var tcs = '';
|
||||
if (m.tool_calls && m.tool_calls.length > 0) {
|
||||
m.tool_calls.forEach(function(tc) {
|
||||
var argsStr = typeof tc.args === 'object' ? JSON.stringify(tc.args, null, 1) : (tc.args || '');
|
||||
var resultStr = tc.result ? (typeof tc.result === 'object' ? JSON.stringify(tc.result, null, 1).substring(0, 200) : String(tc.result).substring(0, 200)) : '';
|
||||
var statusIcon = tc.status === 'denied' ? '⛔' : '🔧';
|
||||
tcs += '<div class="tool-call">'
|
||||
+ '<div><span class="tc-name">' + statusIcon + ' ' + escHtml(tc.tool || tc.name || '') + '</span></div>'
|
||||
+ (argsStr && argsStr !== '{}' ? '<div class="tc-args">' + escHtml(argsStr) + '</div>' : '')
|
||||
+ (resultStr ? '<div class="tc-result">→ ' + escHtml(resultStr) + '</div>' : '')
|
||||
+ '</div>';
|
||||
});
|
||||
}
|
||||
var body = rc + tcs + '<div class="text">' + c + '</div>';
|
||||
if (role === 'system') {
|
||||
html += '<div class="msg msg-system"><div class="msg-bubble">' + body + '</div></div>';
|
||||
} else {
|
||||
html += '<div class="msg msg-' + role + '">'
|
||||
+ '<div class="msg-avatar">' + (role === 'user' ? 'U' : 'A') + '</div>'
|
||||
+ '<div class="msg-content"><div class="msg-bubble">' + body + '</div></div>'
|
||||
+ '</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
msgsEl.innerHTML = html;
|
||||
msgsEl.scrollTop = msgsEl.scrollHeight;
|
||||
updateChatBadge();
|
||||
}
|
||||
|
||||
function updateChatBadge() {
|
||||
var badge = document.getElementById('chat-stage');
|
||||
if (!badge) return;
|
||||
badge.textContent = state.chatStage || '';
|
||||
badge.style.display = state.chatLoading ? 'inline' : 'none';
|
||||
}
|
||||
|
||||
function rerenderChat() { renderChat(); renderChatStarmap(); renderTerminals(); renderCmdHistory() }
|
||||
|
||||
function renderChatStarmap() {
|
||||
var cont = document.getElementById('sm-container-chat');
|
||||
if (!cont) return;
|
||||
if (window._THREE_FAILED || (!window.THREE && window._THREE_FAILED !== undefined)) {
|
||||
cont.innerHTML = '<p style="color:var(--text-muted);padding:20px;text-align:center;font-size:11px">' + __('3D 星图不可用(CDN 加载失败)','Star map unavailable (CDN load failed)') + '</p>';
|
||||
state.starmapInit = true;
|
||||
state.starmapLoading = false;
|
||||
return;
|
||||
}
|
||||
if (!window.THREE) {
|
||||
if (!state.starmapLoading) {
|
||||
cont.innerHTML = '<p style="color:var(--text-muted);padding:20px;text-align:center;font-size:12px">'
|
||||
+ __('Three.js 未加载','Three.js not loaded') + '</p>';
|
||||
state.starmapInit = true;
|
||||
}
|
||||
cont.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;padding:20px"><div class="loading-spinner"></div></div>';
|
||||
state.starmapInit = false;
|
||||
state.starmapLoading = false;
|
||||
return;
|
||||
}
|
||||
if (cont.querySelector('canvas')) {
|
||||
@ -600,6 +658,10 @@ function renderChatStarmap() {
|
||||
if (rect.width > 0) starmapRen.setSize(rect.width, Math.max(rect.height, 250));
|
||||
cont.appendChild(starmapRen.domElement);
|
||||
starmapRen.domElement.style.display = 'block';
|
||||
} else {
|
||||
// starmapRen was destroyed (e.g. re-render cycle), restart
|
||||
state.starmapInit = false;
|
||||
state.starmapLoading = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -635,8 +697,7 @@ async function loadChatStarmapData() {
|
||||
}
|
||||
|
||||
function getStarmapBg() {
|
||||
var isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
||||
return isLight ? 0xf0f4f8 : 0x0a0a1a;
|
||||
return 0x0a0a1a;
|
||||
}
|
||||
|
||||
function initChatStarmap() {
|
||||
@ -651,15 +712,14 @@ function initChatStarmap() {
|
||||
starmapRen.domElement.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
var bg = getStarmapBg();
|
||||
starmapScene = new THREE.Scene();
|
||||
starmapScene.fog = new THREE.FogExp2(bg, 0.02);
|
||||
starmapScene.fog = new THREE.FogExp2(0x0a0a1a, 0.015);
|
||||
starmapCam = new THREE.PerspectiveCamera(60, w / h, 0.1, 2000);
|
||||
starmapCam.position.set(0, 20, 40);
|
||||
starmapRen = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
starmapRen.setSize(w, h);
|
||||
starmapRen.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
starmapRen.setClearColor(bg, 1);
|
||||
starmapRen.setClearColor(0x0a0a1a, 1);
|
||||
cont.innerHTML = '';
|
||||
cont.appendChild(starmapRen.domElement);
|
||||
starmapCtrl = new THREE.OrbitControls(starmapCam, starmapRen.domElement);
|
||||
@ -673,6 +733,7 @@ function initChatStarmap() {
|
||||
dl.position.set(50, 100, 50);
|
||||
starmapScene.add(dl);
|
||||
createStarField();
|
||||
createNebula();
|
||||
buildChatStarmapGraph();
|
||||
starmapRen.domElement.addEventListener('mousemove', onStarmapMove);
|
||||
starmapRen.domElement.addEventListener('click', onStarmapClick);
|
||||
@ -687,111 +748,154 @@ function buildChatStarmapGraph() {
|
||||
starmapNodeMeshes = [];
|
||||
starmapEdgeLines = [];
|
||||
if (starmapNodes.length === 0) return;
|
||||
var nMap = {};
|
||||
starmapNodes.forEach(function(n) { nMap[n.id] = n });
|
||||
var sorted = [...starmapNodes].sort(function(a, b) {
|
||||
// Calculate node degrees for leaf node detection
|
||||
var nodeDegs = {};
|
||||
starmapNodes.forEach(function(n) { nodeDegs[n.id] = 0 });
|
||||
starmapEdges.forEach(function(e) {
|
||||
nodeDegs[e.source_id] = (nodeDegs[e.source_id] || 0) + 1;
|
||||
nodeDegs[e.target_id] = (nodeDegs[e.target_id] || 0) + 1;
|
||||
});
|
||||
var nodeMap = {};
|
||||
starmapNodes.forEach(function(n) { nodeMap[n.id] = n });
|
||||
var sorted = starmapNodes.slice().sort(function(a, b) {
|
||||
return (b.mention_count || 0) - (a.mention_count || 0);
|
||||
});
|
||||
var mc = sorted.map(function(n) { return n.mention_count || 0 });
|
||||
var maxMc = Math.max(...mc, 1), minMc = Math.min(...mc, 0), rng = maxMc - minMc || 1;
|
||||
// Layout positions
|
||||
var pos = {};
|
||||
var baseR = 10, maxR = 50;
|
||||
var baseR = 15, maxR = 80;
|
||||
var total = sorted.length;
|
||||
var acc = 0;
|
||||
sorted.forEach(function(n, i) {
|
||||
var m = n.mention_count || 0, mn = rng > 0 ? (m - minMc) / rng : 0;
|
||||
var rad = baseR + mn * (maxR - baseR);
|
||||
var baseStep = (Math.PI * 2) / sorted.length;
|
||||
var step = baseStep + mn * baseStep * 2;
|
||||
var angle = acc + step / 2;
|
||||
acc += step;
|
||||
var radius = baseR + mn * (maxR - baseR);
|
||||
var baseStep = (Math.PI * 2) / total;
|
||||
var extra = mn * baseStep * 2;
|
||||
var angle = acc + extra / 2;
|
||||
acc += baseStep + extra;
|
||||
pos[n.id] = {
|
||||
x: rad * Math.cos(angle),
|
||||
y: (Math.random() - 0.5) * (5 + mn * 15),
|
||||
z: rad * Math.sin(angle),
|
||||
mn: n,
|
||||
rad: rad
|
||||
x: radius * Math.cos(angle),
|
||||
y: (Math.random() - 0.5) * (10 + mn * 20),
|
||||
z: radius * Math.sin(angle),
|
||||
mn: mn,
|
||||
rad: radius
|
||||
};
|
||||
});
|
||||
// Leaf nodes (degree 1) reposition near parent
|
||||
sorted.forEach(function(n) {
|
||||
var deg = nodeDegs[n.id] || 0;
|
||||
if (deg !== 1) return;
|
||||
var edge = starmapEdges.find(function(e) { return e.source_id === n.id || e.target_id === n.id });
|
||||
if (!edge) return;
|
||||
var parentId = edge.source_id === n.id ? edge.target_id : edge.source_id;
|
||||
if (!pos[parentId]) return;
|
||||
var pp = pos[parentId];
|
||||
var m = n.mention_count || 0, mn = rng > 0 ? (m - minMc) / rng : 0;
|
||||
var off = 6 + mn * 8 + Math.random() * 4;
|
||||
var a2 = Math.random() * Math.PI * 2;
|
||||
pos[n.id] = {
|
||||
x: pp.x + off * Math.cos(a2),
|
||||
y: pp.y + (Math.random() - 0.5) * (4 + mn * 6),
|
||||
z: pp.z + off * Math.sin(a2),
|
||||
mn: mn,
|
||||
rad: off
|
||||
};
|
||||
});
|
||||
// Force-directed simulation
|
||||
for (var it = 0; it < 50; it++) {
|
||||
Object.keys(pos).forEach(function(i) {
|
||||
Object.keys(pos).forEach(function(j) {
|
||||
if (i >= j) return;
|
||||
var a = pos[i], b = pos[j];
|
||||
var ids = Object.keys(pos);
|
||||
// Repulsion
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
for (var j = i + 1; j < ids.length; j++) {
|
||||
var a = pos[ids[i]], b = pos[ids[j]];
|
||||
var dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z, d = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
|
||||
var f = (0.5 + (a.mn + b.mn) * 0.5);
|
||||
if (d < 20) {
|
||||
var force = (0.08 * f) / Math.max(d, 0.5);
|
||||
var rf = 0.5 + (a.mn + b.mn) * 0.5;
|
||||
if (d < 25) {
|
||||
var force = (0.06 * rf) / Math.max(d, 0.5);
|
||||
a.x += dx / d * force; a.y += dy / d * force; a.z += dz / d * force;
|
||||
b.x -= dx / d * force; b.y -= dy / d * force; b.z -= dz / d * force;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
// Attraction along edges
|
||||
starmapEdges.forEach(function(e) {
|
||||
var a = pos[e.source_id], b = pos[e.target_id];
|
||||
if (!a || !b) return;
|
||||
var dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z, d = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
|
||||
if (d > 15) {
|
||||
var f = 0.05 * Math.max(0.3, 1 - (a.mn + b.mn) * 0.3);
|
||||
a.x += dx / d * f; a.y += dy / d * f; a.z += dz / d * f;
|
||||
b.x -= dx / d * f; b.y -= dy / d * f; b.z -= dz / d * f;
|
||||
var af = Math.max(0.3, 1.0 - (a.mn + b.mn) * 0.3);
|
||||
if (d > 20) {
|
||||
var force = 0.04 * af;
|
||||
a.x += dx / d * force; a.y += dy / d * force; a.z += dz / d * force;
|
||||
b.x -= dx / d * force; b.y -= dy / d * force; b.z -= dz / d * force;
|
||||
}
|
||||
});
|
||||
Object.keys(pos).forEach(function(i) {
|
||||
var p = pos[i];
|
||||
var d = Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z);
|
||||
// Centering constraint
|
||||
ids.forEach(function(id) {
|
||||
var p = pos[id];
|
||||
var dist = Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z);
|
||||
var maxA = maxR * 1.5;
|
||||
if (d > maxA) {
|
||||
var s = maxA / d;
|
||||
p.x *= s; p.y *= s; p.z *= s;
|
||||
}
|
||||
if (dist > maxA) { var s = maxA / dist; p.x *= s; p.y *= s; p.z *= s }
|
||||
});
|
||||
}
|
||||
// Create nodes
|
||||
starmapNodes.forEach(function(n) {
|
||||
var p = pos[n.id];
|
||||
if (!p) return;
|
||||
var mn = n.mention_count || 0, mnr = rng > 0 ? (mn - minMc) / rng : 0;
|
||||
var rad = 0.4 + mnr * 1.5;
|
||||
var rad = 0.5 + mnr * 2.0;
|
||||
var col = smTypeColors[n.type] || 0xcccccc;
|
||||
var ei = 0.3 + mnr * 0.7;
|
||||
var g = new THREE.SphereGeometry(rad, 16, 12);
|
||||
var mat = new THREE.MeshPhongMaterial({ color: col, emissive: col, emissiveIntensity: ei });
|
||||
var mat = new THREE.MeshPhongMaterial({ color: col, emissive: col, emissiveIntensity: ei, shininess: 30 });
|
||||
var mesh = new THREE.Mesh(g, mat);
|
||||
mesh.position.set(p.x, p.y, p.z);
|
||||
mesh.userData.nodeData = n;
|
||||
mesh.userData.nodeId = n.id;
|
||||
starmapScene.add(mesh);
|
||||
starmapNodeMeshes.push(mesh);
|
||||
// label sprite
|
||||
mesh.userData.baseEmissive = ei;
|
||||
// Glow sphere
|
||||
var gr = rad * 1.2 + mnr * 0.5;
|
||||
var gg = new THREE.SphereGeometry(gr, 16, 12);
|
||||
var gm = new THREE.MeshBasicMaterial({ color: col, transparent: true, opacity: 0.12 + mnr * 0.08, side: THREE.BackSide, blending: THREE.AdditiveBlending });
|
||||
var gs = new THREE.Mesh(gg, gm);
|
||||
mesh.add(gs);
|
||||
mesh.userData.glowSphere = gs;
|
||||
// Label sprite
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = 256;
|
||||
canvas.height = 64;
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'rgba(0,0,0,0)';
|
||||
ctx.fillRect(0, 0, 256, 64);
|
||||
ctx.font = 'Bold 28px Arial';
|
||||
ctx.clearRect(0, 0, 256, 64);
|
||||
ctx.font = 'Bold 24px Courier New';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.85)';
|
||||
ctx.fillText(n.name || n.id, 128, 34);
|
||||
ctx.shadowColor = '#aaccff';
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillText((n.name || n.id).substring(0, 12), 128, 32);
|
||||
var tex = new THREE.CanvasTexture(canvas);
|
||||
var spMat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
|
||||
tex.needsUpdate = true;
|
||||
var spMat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.9, depthTest: false, depthWrite: false, blending: THREE.AdditiveBlending });
|
||||
var sprite = new THREE.Sprite(spMat);
|
||||
sprite.position.set(p.x, p.y + rad + 1.5, p.z);
|
||||
sprite.scale.set(6, 1.5, 1);
|
||||
starmapScene.add(sprite);
|
||||
starmapNodeMeshes.push(sprite);
|
||||
sprite.scale.set(8, 2, 1);
|
||||
sprite.position.y = rad + 2;
|
||||
mesh.add(sprite);
|
||||
starmapScene.add(mesh);
|
||||
starmapNodeMeshes.push(mesh);
|
||||
});
|
||||
// Create edges
|
||||
starmapEdges.forEach(function(e) {
|
||||
var a = pos[e.source_id], b = pos[e.target_id];
|
||||
if (!a || !b) return;
|
||||
var col = smEdgeColors[e.relation_type] || smEdgeColors[e.type] || 0x444466;
|
||||
var pts = [
|
||||
new THREE.Vector3(a.x, a.y, a.z),
|
||||
new THREE.Vector3(b.x, b.y, b.z)
|
||||
];
|
||||
var col = smEdgeColors[e.type] || 0x888888;
|
||||
var geo = new THREE.BufferGeometry().setFromPoints(pts);
|
||||
var mat = new THREE.LineBasicMaterial({ color: col, transparent: true, opacity: 0.35 });
|
||||
var mat = new THREE.LineBasicMaterial({ color: col, transparent: true, opacity: 0.4 });
|
||||
var line = new THREE.Line(geo, mat);
|
||||
line.userData = { edgeId: e.id, edgeData: e };
|
||||
starmapScene.add(line);
|
||||
starmapEdgeLines.push(line);
|
||||
});
|
||||
@ -806,12 +910,17 @@ async function sendChat() {
|
||||
inp.value = '';
|
||||
rerenderChat();
|
||||
state.chatLoading = true;
|
||||
state.chatStage = __('等待AI回复...','Waiting for AI...');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
btn.textContent = '';
|
||||
rerenderChat();
|
||||
try {
|
||||
var r = await api('/chat', { method: 'POST', body: JSON.stringify({ message: text }) });
|
||||
state.chatStage = '';
|
||||
var last = state.messages[state.messages.length - 1];
|
||||
console.log('[sendChat] POST returned, last msg:', last ? {role:last.role, _streaming:last._streaming, _final:last._final, tool_calls:last.tool_calls?.length, content_len:last.content?.length} : null);
|
||||
if (last && last.role === 'assistant' && last._streaming) {
|
||||
console.log('[sendChat] updating existing streaming msg, tool_calls before:', last.tool_calls?.length);
|
||||
last.content = r.response || __('(无响应)','(no response)');
|
||||
last.reasoning_content = r.reasoning_content || '';
|
||||
last._final = true;
|
||||
@ -832,8 +941,10 @@ async function sendChat() {
|
||||
toast(__('请求失败: ','Request failed: ') + e.message, true);
|
||||
} finally {
|
||||
state.chatLoading = false;
|
||||
state.chatStage = '';
|
||||
btn.disabled = false;
|
||||
btn.textContent = '发送';
|
||||
btn.textContent = __('发送','Send');
|
||||
rerenderChat();
|
||||
}
|
||||
}
|
||||
|
||||
@ -996,23 +1107,26 @@ function renderCmdHistory() {
|
||||
}
|
||||
|
||||
function connectSSE() {
|
||||
if (state.eventSource) state.eventSource.close();
|
||||
if (state.eventSource) { console.log('[SSE] closing old connection'); state.eventSource.close() }
|
||||
var es;
|
||||
try { es = new EventSource('/api/v1/chat/events'); state.eventSource = es } catch(ex) {}
|
||||
if (!es) return;
|
||||
try { es = new EventSource('/api/v1/chat/events', { withCredentials: true }); state.eventSource = es } catch(ex) { console.error('[SSE] create failed', ex); return }
|
||||
if (!es) { console.error('[SSE] es is null'); return }
|
||||
console.log('[SSE] connected');
|
||||
es.addEventListener('agent_output', function(e) {
|
||||
try {
|
||||
var ev = JSON.parse(e.data);
|
||||
var p = ev.payload || {};
|
||||
console.log('[SSE] agent_output received', p.content ? p.content.substring(0,50) : '(empty)');
|
||||
if (!p.content) return;
|
||||
state.chatStage = __('AI 回复中...','AI replying...');
|
||||
if (state.messages.length > 0 && state.messages[state.messages.length - 1].role === 'assistant' && !state.messages[state.messages.length - 1]._final) {
|
||||
state.messages[state.messages.length - 1].content += p.content;
|
||||
renderChat(); renderChatStarmap();
|
||||
rerenderChat();
|
||||
return;
|
||||
}
|
||||
state.messages.push({ role: 'assistant', content: p.content, _streaming: true });
|
||||
renderChat(); renderChatStarmap();
|
||||
} catch(ex) {}
|
||||
rerenderChat();
|
||||
} catch(ex) { console.error('[SSE] agent_output error', ex) }
|
||||
});
|
||||
es.addEventListener('reasoning', function(e) {
|
||||
try {
|
||||
@ -1021,8 +1135,9 @@ function connectSSE() {
|
||||
if (p.content && state.messages.length > 0) {
|
||||
var last = state.messages[state.messages.length - 1];
|
||||
if (last.role === 'assistant') {
|
||||
state.chatStage = __('AI 思考中...','AI thinking...');
|
||||
last.reasoning_content = (last.reasoning_content || '') + p.content;
|
||||
renderChat(); renderChatStarmap();
|
||||
rerenderChat();
|
||||
}
|
||||
}
|
||||
} catch(ex) {}
|
||||
@ -1031,23 +1146,62 @@ function connectSSE() {
|
||||
try {
|
||||
var ev = JSON.parse(e.data);
|
||||
var p = ev.payload || {};
|
||||
console.log('[SSE] tool_call', p);
|
||||
if (!p.tool) return;
|
||||
var last = state.messages.length > 0 ? state.messages[state.messages.length - 1] : null;
|
||||
if (last && last.role === 'assistant') {
|
||||
if (!last.tool_calls) last.tool_calls = [];
|
||||
last.tool_calls.push({
|
||||
tool: p.tool,
|
||||
name: p.tool,
|
||||
args: p.args || {},
|
||||
result: p.result || '',
|
||||
status: p.status || 'ok',
|
||||
plugin: p.plugin || ''
|
||||
});
|
||||
renderChat(); renderChatStarmap();
|
||||
if (!last || last.role !== 'assistant') {
|
||||
state.messages.push({ role: 'assistant', content: '', tool_calls: [], _streaming: true });
|
||||
last = state.messages[state.messages.length - 1];
|
||||
}
|
||||
if (!last.tool_calls) last.tool_calls = [];
|
||||
last.tool_calls.push({
|
||||
tool: p.tool,
|
||||
name: p.tool,
|
||||
args: p.args || {},
|
||||
result: p.result || '',
|
||||
status: p.status || 'ok',
|
||||
plugin: p.plugin || ''
|
||||
});
|
||||
state.chatStage = __('工具调用: ','Tool: ') + (p.tool || '');
|
||||
rerenderChat();
|
||||
} catch(ex) { console.error('[SSE] tool_call error', ex) }
|
||||
});
|
||||
es.addEventListener('tool_result', function(e) {
|
||||
try {
|
||||
var ev = JSON.parse(e.data);
|
||||
var p = ev.payload || {};
|
||||
state.chatStage = __('工具结果已返回','Tool result received');
|
||||
var badge = document.getElementById('chat-stage');
|
||||
if (badge) badge.textContent = state.chatStage;
|
||||
} catch(ex) {}
|
||||
});
|
||||
es.onerror = function() { setTimeout(connectSSE, 5000) };
|
||||
es.addEventListener('stage', function(e) {
|
||||
try {
|
||||
var ev = JSON.parse(e.data);
|
||||
var p = ev.payload || {};
|
||||
var phase = p.phase || '';
|
||||
var tool = p.tool || '';
|
||||
console.log('[SSE] stage event', phase, tool);
|
||||
if (phase === 'pre_action') {
|
||||
state.chatStage = __('AI 思考中...','AI thinking...');
|
||||
} else if (phase === 'before_toolcall') {
|
||||
state.chatStage = __('工具调用: ','Tool: ') + (tool || '');
|
||||
} else if (phase === 'before_output') {
|
||||
state.chatStage = __('生成回复中...','Generating response...');
|
||||
}
|
||||
var badge = document.getElementById('chat-stage');
|
||||
if (badge) { badge.textContent = state.chatStage || ''; badge.style.display = state.chatLoading ? 'inline' : 'none' }
|
||||
} catch(ex) { console.error('[SSE] stage error', ex) }
|
||||
});
|
||||
es.onopen = function() { console.log('[SSE] connection opened') };
|
||||
es.onerror = function(e) { console.error('[SSE] error', e); setTimeout(connectSSE, 5000) };
|
||||
// Periodically refresh sidebar data
|
||||
if (state._sidebarRefresh) clearInterval(state._sidebarRefresh);
|
||||
state._sidebarRefresh = setInterval(async function() {
|
||||
try { var td = await api('/terminals'); if (td && td.terminals) state.terminals = td.terminals } catch(e) {}
|
||||
try { var ch = await api('/cmd/history'); if (ch && ch.history) state.cmdHistory = ch.history } catch(e) {}
|
||||
renderTerminals(); renderCmdHistory();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// ===== Plugins =====
|
||||
@ -1329,10 +1483,11 @@ function flyStarmapTo(nodeId, dur) {
|
||||
}
|
||||
|
||||
function onStarmapResize() {
|
||||
var cont = document.getElementById('starmap-container');
|
||||
if (!cont || !starmapRen || !starmapCam) return;
|
||||
if (!starmapRen || !starmapCam) return;
|
||||
var cont = starmapRen.domElement.parentElement;
|
||||
if (!cont) return;
|
||||
var rect = cont.getBoundingClientRect();
|
||||
var w = rect.width || 800, h = Math.max(rect.height || 500, 100);
|
||||
var w = rect.width || 800, h = Math.max(rect.height || 250, 100);
|
||||
if (w > 0 && h > 0) { starmapCam.aspect = w / h; starmapCam.updateProjectionMatrix(); starmapRen.setSize(w, h) }
|
||||
}
|
||||
|
||||
@ -1343,8 +1498,10 @@ function toggleStarmapAuto() {
|
||||
}
|
||||
|
||||
function resetStarmapCamera() {
|
||||
if (!starmapCam || !starmapCtrl || !starmapNodeMeshes) return;
|
||||
var maxD = 0;
|
||||
starmapNodeMeshes.forEach(function(m) { var d = m.position.length(); if (d > maxD) maxD = d });
|
||||
if (maxD < 1) maxD = 30;
|
||||
var td = Math.min(Math.max(maxD + 20, 30), 150);
|
||||
var sp = starmapCam.position.clone(), ep = new THREE.Vector3(td * 0.9, td * 0.6, td * 0.9);
|
||||
var st = starmapCtrl.target.clone(), t0 = Date.now();
|
||||
@ -1363,6 +1520,31 @@ function starmapAnimate() {
|
||||
if (starmapRen && starmapScene && starmapCam) starmapRen.render(starmapScene, starmapCam);
|
||||
}
|
||||
|
||||
function createNebula() {
|
||||
var nc = 500;
|
||||
var p = new Float32Array(nc * 3), cl = new Float32Array(nc * 3);
|
||||
for (var i = 0; i < nc; i++) {
|
||||
var i3 = i * 3;
|
||||
p[i3] = (Math.random() - 0.5) * 800;
|
||||
p[i3+1] = (Math.random() - 0.5) * 800;
|
||||
p[i3+2] = (Math.random() - 0.5) * 800;
|
||||
var ch = Math.random();
|
||||
if (ch < 0.33) {
|
||||
cl[i3]=0.5+Math.random()*0.3; cl[i3+1]=0.2+Math.random()*0.2; cl[i3+2]=0.7+Math.random()*0.3;
|
||||
} else if (ch < 0.66) {
|
||||
cl[i3]=0.2+Math.random()*0.2; cl[i3+1]=0.3+Math.random()*0.3; cl[i3+2]=0.8+Math.random()*0.2;
|
||||
} else {
|
||||
cl[i3]=0.7+Math.random()*0.3; cl[i3+1]=0.2+Math.random()*0.2; cl[i3+2]=0.5+Math.random()*0.3;
|
||||
}
|
||||
}
|
||||
var g = new THREE.BufferGeometry();
|
||||
g.setAttribute('position', new THREE.BufferAttribute(p, 3));
|
||||
g.setAttribute('color', new THREE.BufferAttribute(cl, 3));
|
||||
var m = new THREE.PointsMaterial({ size: 8, vertexColors: true, transparent: true, opacity: 0.15, sizeAttenuation: true, blending: THREE.AdditiveBlending });
|
||||
var np = new THREE.Points(g, m);
|
||||
starmapScene.add(np);
|
||||
}
|
||||
|
||||
// ===== Settings =====
|
||||
function pluginDisplayName(p) {
|
||||
if (p === 'core') return __('核心', 'Core');
|
||||
@ -1664,7 +1846,7 @@ async function logout() {
|
||||
|
||||
renderConfigDisabled();
|
||||
connectSSE();
|
||||
(async function() { await loadChatHistory(); renderAll() })();
|
||||
(async function() { await loadChatHistory(); renderAll(); startUptimeTicker() })();
|
||||
setInterval(renderAll, 15000);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
@ -131,12 +132,35 @@ func NewHandler(sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager,
|
||||
sessions: make(map[string]time.Time),
|
||||
termStates: make(map[string]*termState),
|
||||
}
|
||||
h.loadChatHistory()
|
||||
if evBus != nil {
|
||||
go h.trackToolEvents()
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *Handler) loadChatHistory() {
|
||||
if h.cfgReg == nil {
|
||||
return
|
||||
}
|
||||
ps := h.cfgReg.PluginConfig("webui")
|
||||
v, err := ps.Get("chathistory")
|
||||
if err != nil || v == nil {
|
||||
return
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok || s == "" {
|
||||
return
|
||||
}
|
||||
var msgs []ChatMsg
|
||||
if err := json.Unmarshal([]byte(s), &msgs); err != nil {
|
||||
return
|
||||
}
|
||||
h.chatMu.Lock()
|
||||
h.chatHistory = msgs
|
||||
h.chatMu.Unlock()
|
||||
}
|
||||
|
||||
func (h *Handler) trackToolEvents() {
|
||||
h.eventBus.Subscribe(events.EventToolCall, func(ev *events.Event) {
|
||||
h.handleToolEvent(ev)
|
||||
@ -408,7 +432,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
agents := h.supervisor.ListAgents()
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"status": "running",
|
||||
"uptime": time.Since(h.startTime).String(),
|
||||
"uptime": time.Since(h.startTime).Round(time.Second).String(),
|
||||
"agents": len(agents),
|
||||
"version": meta.Version,
|
||||
"startedAt": h.startTime,
|
||||
@ -843,11 +867,17 @@ func (h *Handler) handleNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *Handler) addChatMsg(msg ChatMsg) {
|
||||
h.chatMu.Lock()
|
||||
defer h.chatMu.Unlock()
|
||||
h.chatHistory = append(h.chatHistory, msg)
|
||||
if len(h.chatHistory) > maxChatHistory {
|
||||
h.chatHistory = h.chatHistory[len(h.chatHistory)-maxChatHistory:]
|
||||
}
|
||||
// persist to webui config table as compact JSON
|
||||
if h.cfgReg != nil {
|
||||
ps := h.cfgReg.PluginConfig("webui")
|
||||
b, _ := json.Marshal(h.chatHistory)
|
||||
ps.Set("chathistory", string(b))
|
||||
}
|
||||
h.chatMu.Unlock()
|
||||
}
|
||||
|
||||
func (h *Handler) handleChatHistory(w http.ResponseWriter, r *http.Request) {
|
||||
@ -944,23 +974,36 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
|
||||
defer close(writeCh)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[SSE] writer panic: %v", r)
|
||||
}
|
||||
}()
|
||||
for line := range writeCh {
|
||||
fmt.Fprintf(w, "%s\n", line)
|
||||
flusher.Flush()
|
||||
}
|
||||
}()
|
||||
|
||||
subTypes := []string{"agent_output", "reasoning", "agent_error", "tool_call"}
|
||||
subTypes := []string{"agent_output", "reasoning", "agent_error", "tool_call", "stage", "agent_llm_chain"}
|
||||
var unsubs []func()
|
||||
for _, t := range subTypes {
|
||||
t2 := t
|
||||
_ = h.eventBus.Subscribe(events.EventType(t2), func(evt *events.Event) {
|
||||
unsub := h.eventBus.Subscribe(events.EventType(t2), func(evt *events.Event) {
|
||||
data, _ := json.Marshal(evt)
|
||||
select {
|
||||
case writeCh <- fmt.Sprintf("event: %s\ndata: %s", evt.Type, string(data)):
|
||||
case writeCh <- fmt.Sprintf("event: %s\ndata: %s\n", evt.Type, string(data)):
|
||||
default:
|
||||
log.Printf("[SSE] DROPPED event %s (writeCh full, len=%d)", evt.Type, len(writeCh))
|
||||
}
|
||||
})
|
||||
unsubs = append(unsubs, unsub)
|
||||
}
|
||||
defer func() {
|
||||
for _, unsub := range unsubs {
|
||||
unsub()
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
|
||||
@ -155,11 +155,30 @@ func (p *Plugin) ensureAuthBootstrap(s *sdk.PluginSDK) {
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "api_key", Default: "", Type: "password", DisplayName: "API 密钥", Description: "访问 API 时需要的密钥", Category: "webui"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "username", Default: "admin", Type: "string", DisplayName: "登录用户名", Description: "Web 控制台登录用户名", Category: "webui"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "password", Default: "", Type: "password", DisplayName: "Web 控制台登录密码", Description: "Web 控制台登录密码", Category: "webui"})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "session_ttl_hours", Default: "24", Type: "int", DisplayName: "会话时长(小时)", Description: "登录 cookie 有效时长", Category: "webui"})
|
||||
p.ensureAuthBootstrap(s)
|
||||
|
||||
s.RegisterStage(sdk.StagePreAction, func(ctx *sdk.StageContext) error {
|
||||
p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "pre_action", "message": "thinking"}})
|
||||
return nil
|
||||
})
|
||||
s.RegisterStage(sdk.StageBeforeToolcall, func(ctx *sdk.StageContext) error {
|
||||
tool := ""
|
||||
if len(ctx.ToolCalls) > 0 {
|
||||
tool = ctx.ToolCalls[0].Name
|
||||
}
|
||||
p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "before_toolcall", "tool": tool, "message": "tool:" + tool}})
|
||||
return nil
|
||||
})
|
||||
s.RegisterStage(sdk.StageBeforeOutput, func(ctx *sdk.StageContext) error {
|
||||
p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "before_output", "message": "output"}})
|
||||
return nil
|
||||
})
|
||||
|
||||
h := NewHandler(p.sup, p.mem, p.sk, p.lua, p.cfg, p.iom, p.tm, p.ks, p.tr, p.cr, p.pr, p.evBus, p.statusProvider, p.providerMgr, p.baseAPIKey)
|
||||
p.handler = h
|
||||
h.RegisterRoutes(p.mux)
|
||||
|
||||
@ -38,6 +38,7 @@ type IOInjector = pubsdk.IOInjector
|
||||
type ToolRegistrar = pubsdk.ToolRegistrar
|
||||
type StageRegistrar = pubsdk.StageRegistrar
|
||||
type APIRegistrar = pubsdk.APIRegistrar
|
||||
type OutputChannelRegistrar = pubsdk.OutputChannelRegistrar
|
||||
|
||||
type PluginSDK struct {
|
||||
*pubsdk.PluginSDK
|
||||
@ -68,23 +69,36 @@ func (a ioAdapter) InjectTextNoMemory(source, channel, text string) {
|
||||
}
|
||||
}
|
||||
|
||||
func New(name string, iom *agentIO.IOManager, eventBus *events.Bus, mem MemoryAPI,
|
||||
textMem TextMemoryAPI, docMem DocMemoryAPI, know KnowledgeAPI, llm LLMAPI,
|
||||
sett SettingsAPI, regTool ToolRegistrar, regStage StageRegistrar, regAPI APIRegistrar,
|
||||
) *PluginSDK {
|
||||
base := pubsdk.New(name, sett, regTool, regStage, regAPI)
|
||||
if iom != nil {
|
||||
base.SetIOInjector(ioAdapter{iom: iom})
|
||||
// SDKConfig holds all dependencies for creating a PluginSDK.
|
||||
type SDKConfig struct {
|
||||
IOManager *agentIO.IOManager
|
||||
EventBus *events.Bus
|
||||
Memory MemoryAPI
|
||||
TextMemory TextMemoryAPI
|
||||
DocMemory DocMemoryAPI
|
||||
Knowledge KnowledgeAPI
|
||||
LLM LLMAPI
|
||||
Settings SettingsAPI
|
||||
RegTool ToolRegistrar
|
||||
RegStage StageRegistrar
|
||||
RegAPI APIRegistrar
|
||||
RegOutput OutputChannelRegistrar
|
||||
}
|
||||
|
||||
func New(name string, cfg SDKConfig) *PluginSDK {
|
||||
base := pubsdk.New(name, cfg.Settings, cfg.RegTool, cfg.RegStage, cfg.RegAPI, cfg.RegOutput)
|
||||
if cfg.IOManager != nil {
|
||||
base.SetIOInjector(ioAdapter{iom: cfg.IOManager})
|
||||
}
|
||||
base.SetMemoryAPI(mem)
|
||||
base.SetTextMemoryAPI(textMem)
|
||||
base.SetDocMemoryAPI(docMem)
|
||||
base.SetKnowledgeAPI(know)
|
||||
base.SetLLMAPI(llm)
|
||||
base.SetMemoryAPI(cfg.Memory)
|
||||
base.SetTextMemoryAPI(cfg.TextMemory)
|
||||
base.SetDocMemoryAPI(cfg.DocMemory)
|
||||
base.SetKnowledgeAPI(cfg.Knowledge)
|
||||
base.SetLLMAPI(cfg.LLM)
|
||||
return &PluginSDK{
|
||||
PluginSDK: base,
|
||||
iom: iom,
|
||||
eventBus: eventBus,
|
||||
iom: cfg.IOManager,
|
||||
eventBus: cfg.EventBus,
|
||||
logger: log.Default(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user