ym-hermes-desktop 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -2
- package/package.json +2 -3
- package/src/config.js +1 -0
- package/src/hermes.js +63 -0
- package/src/index.js +16 -1
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ HERMES_MODEL=hermes-agent
|
|
|
27
27
|
- `HERMES_API_ENDPOINT`:Hermes API server 地址(默认本地 8642)
|
|
28
28
|
- `HERMES_API_KEY`:Hermes 的 `API_SERVER_KEY`(与 `~/.hermes/.env` 一致)
|
|
29
29
|
- `HERMES_MODEL`:`/v1/models` 返回的模型 id(默认 `hermes-agent`)
|
|
30
|
+
- `REPORT_URL`:可选。任务完成上报接口(POST),配置后 `dispatch` 异步任务执行完自动上报
|
|
30
31
|
|
|
31
32
|
## 使用
|
|
32
33
|
|
|
@@ -37,7 +38,25 @@ npm start # 交互模式:直接输入问题,Hermes 执行并回答
|
|
|
37
38
|
npm run ask "帮我查一下今天杭州天气" # 单次提问
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
交互模式内可用 `/models`、`/check`、`/help`、`/exit
|
|
41
|
+
交互模式内可用 `/models`、`/check`、`/help`、`/exit`,以及 `/dispatch <任务名> <任务内容...>`(立即返回「任务名 已下达」,后台执行完自动上报)。
|
|
42
|
+
|
|
43
|
+
## 异步下发(下发即确认,完成后自动上报)
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import { dispatch } from './src/hermes.js';
|
|
47
|
+
|
|
48
|
+
// 调用后立即返回确认,不等任务执行完
|
|
49
|
+
const ack = dispatch({
|
|
50
|
+
taskName: '周报生成',
|
|
51
|
+
content: '请总结今天的工作生成周报',
|
|
52
|
+
reportUrl: 'http://你的接口/report', // 可选;不传则用 .env 的 REPORT_URL
|
|
53
|
+
});
|
|
54
|
+
// ack = { task_name: '周报生成', status: '已下达', received_at: '2026-09-12 22:52:21' }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
后台执行流程:异步调 Hermes 完成任务 → 组装 `{ task_name, summary, executed_at(YYYY-MM-DD HH:mm:ss), duration_seconds }` → POST 到 `reportUrl`(或 `.env` 的 `REPORT_URL`)。
|
|
58
|
+
|
|
59
|
+
配合独立上报工具 `ym-hermes-task`(注册进 Hermes 的 MCP 工具 `report_task`)可双保险:客户端上报 + Hermes 任务结束主动上报。
|
|
41
60
|
|
|
42
61
|
## 作为模块接入你自己的 AI 工具
|
|
43
62
|
|
|
@@ -88,7 +107,7 @@ const r = await chat({ messages: [{ role: 'user', content: '你好' }] });
|
|
|
88
107
|
"mcpServers": {
|
|
89
108
|
"ym-hermes": {
|
|
90
109
|
"command": "npx",
|
|
91
|
-
"args": ["-y", "ym-hermes-desktop"
|
|
110
|
+
"args": ["-y", "ym-hermes-desktop"],
|
|
92
111
|
"env": {
|
|
93
112
|
"HERMES_API_ENDPOINT": "http://127.0.0.1:8642/v1",
|
|
94
113
|
"HERMES_API_KEY": "你的 Bearer token",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ym-hermes-desktop",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Hermes Agent 接入点客户端与 MCP server:把 Hermes(含其 MCP 工具)通过 OpenAI 兼容 API / MCP stdio 协议暴露给任意 AI 工具。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/hermes.js",
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
"./server": "./src/mcp-server.js"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
|
-
"ym-hermes": "src/
|
|
15
|
-
"ym-hermes-mcp": "src/mcp-server.js"
|
|
14
|
+
"ym-hermes": "src/mcp-server.js"
|
|
16
15
|
},
|
|
17
16
|
"files": [
|
|
18
17
|
"src",
|
package/src/config.js
CHANGED
|
@@ -40,5 +40,6 @@ export function loadEnv() {
|
|
|
40
40
|
endpoint: (process.env.HERMES_API_ENDPOINT || dot.HERMES_API_ENDPOINT || '').replace(/\/+$/, ''),
|
|
41
41
|
apiKey: process.env.HERMES_API_KEY || dot.HERMES_API_KEY || '',
|
|
42
42
|
model: process.env.HERMES_MODEL || dot.HERMES_MODEL || 'hermes-agent',
|
|
43
|
+
reportUrl: (process.env.REPORT_URL || dot.REPORT_URL || '').replace(/\/+$/, ''),
|
|
43
44
|
};
|
|
44
45
|
}
|
package/src/hermes.js
CHANGED
|
@@ -8,6 +8,12 @@ import { loadEnv } from './config.js';
|
|
|
8
8
|
|
|
9
9
|
const { endpoint, apiKey, model } = loadEnv();
|
|
10
10
|
|
|
11
|
+
/** 当前时间,格式 YYYY-MM-DD HH:mm:ss(本地时区,精确到秒) */
|
|
12
|
+
export function nowString(date = new Date()) {
|
|
13
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
14
|
+
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
/** 发起一次 chat 请求(非流式) */
|
|
12
18
|
export async function chat({ messages, tools = [], toolChoice = undefined, temperature = undefined }) {
|
|
13
19
|
if (!endpoint || !apiKey) {
|
|
@@ -59,3 +65,60 @@ export function summarizeChoice(choice) {
|
|
|
59
65
|
if (lines.length === 0) lines.push('(无文本输出)');
|
|
60
66
|
return lines.join('\n');
|
|
61
67
|
}
|
|
68
|
+
|
|
69
|
+
/** 秒级耗时 */
|
|
70
|
+
function durationSeconds(ms) {
|
|
71
|
+
return Math.round(ms / 10) / 100;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 异步下发任务:立即返回「任务名 已下达」,后台执行 Hermes,完成后自动上报。
|
|
76
|
+
*
|
|
77
|
+
* @param {object} opts
|
|
78
|
+
* @param {string} opts.taskName 任务名称(会出现在确认与上报中)
|
|
79
|
+
* @param {string} opts.content 要 Hermes 执行的任务内容
|
|
80
|
+
* @param {string} [opts.reportUrl] 上报接口 URL(默认取 .env 的 REPORT_URL)
|
|
81
|
+
* @param {Function} [opts.onDone] 可选回调 (payload) => void,任务完成上报后调用
|
|
82
|
+
* @returns {{task_name:string, status:string, received_at:string}} 立即返回的确认
|
|
83
|
+
*/
|
|
84
|
+
export function dispatch({ taskName, content, reportUrl, onDone } = {}) {
|
|
85
|
+
if (!taskName || !content) throw new Error('dispatch 需要 taskName 和 content');
|
|
86
|
+
const ack = { task_name: taskName, status: '已下达', received_at: nowString() };
|
|
87
|
+
|
|
88
|
+
runTaskAsync({ taskName, content, reportUrl, onDone }).catch((err) => {
|
|
89
|
+
console.error(`[dispatch:${taskName}] 任务执行/上报失败: ${err.message}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return ack;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function runTaskAsync({ taskName, content, reportUrl, onDone }) {
|
|
96
|
+
const startedMs = Date.now();
|
|
97
|
+
const result = await chat({ messages: [{ role: 'user', content }] });
|
|
98
|
+
const summary = result?.choices?.[0]?.message?.content || '(无文本结果)';
|
|
99
|
+
|
|
100
|
+
const payload = {
|
|
101
|
+
task_name: taskName,
|
|
102
|
+
summary,
|
|
103
|
+
executed_at: nowString(),
|
|
104
|
+
duration_seconds: durationSeconds(Date.now() - startedMs),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const url = reportUrl || loadEnv().reportUrl;
|
|
108
|
+
if (url) {
|
|
109
|
+
const resp = await fetch(url, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'Content-Type': 'application/json' },
|
|
112
|
+
body: JSON.stringify(payload),
|
|
113
|
+
});
|
|
114
|
+
if (!resp.ok) {
|
|
115
|
+
throw new Error(`上报失败 HTTP ${resp.status}: ${(await resp.text()).slice(0, 300)}`);
|
|
116
|
+
}
|
|
117
|
+
console.log(`[dispatch:${taskName}] 完成并已上报 (HTTP ${resp.status})`);
|
|
118
|
+
} else {
|
|
119
|
+
console.log(`[dispatch:${taskName}] 完成(未配置 REPORT_URL,跳过上报)`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (onDone) onDone(payload);
|
|
123
|
+
return payload;
|
|
124
|
+
}
|
package/src/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 你自己的 AI 工具可直接 import src/hermes.js 使用。
|
|
13
13
|
*/
|
|
14
14
|
import readline from 'node:readline';
|
|
15
|
-
import { chat, listModels, summarizeChoice } from './hermes.js';
|
|
15
|
+
import { chat, listModels, summarizeChoice, dispatch } from './hermes.js';
|
|
16
16
|
import { loadEnv } from './config.js';
|
|
17
17
|
|
|
18
18
|
function printHelp() {
|
|
@@ -99,6 +99,21 @@ async function main() {
|
|
|
99
99
|
}
|
|
100
100
|
} else if (input === '/check') {
|
|
101
101
|
await checkHealth();
|
|
102
|
+
} else if (input.startsWith('dispatch ')) {
|
|
103
|
+
// dispatch <任务名> <任务内容...>
|
|
104
|
+
const parts = input.slice('dispatch '.length).split(/\s+/);
|
|
105
|
+
const taskName = parts.shift();
|
|
106
|
+
const content = parts.join(' ');
|
|
107
|
+
if (!taskName || !content) {
|
|
108
|
+
console.log('用法: dispatch <任务名> <任务内容...>');
|
|
109
|
+
} else {
|
|
110
|
+
try {
|
|
111
|
+
const ack = dispatch({ taskName, content });
|
|
112
|
+
console.log(`✓ ${ack.task_name} ${ack.status}(${ack.received_at}),后台执行中,完成后自动上报`);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.log(`✗ ${err.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
102
117
|
} else {
|
|
103
118
|
try {
|
|
104
119
|
await askOnce(input);
|