@hirey/hi-mcp-server 0.1.9 → 0.1.11
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 +4 -3
- package/dist/defaultReplyRoute.d.ts +15 -0
- package/dist/defaultReplyRoute.d.ts.map +1 -0
- package/dist/defaultReplyRoute.js +28 -0
- package/dist/receiver-config-material.d.ts +4 -0
- package/dist/receiver-config-material.d.ts.map +1 -0
- package/dist/receiver-config-material.js +47 -0
- package/dist/server.js +103 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,9 +91,10 @@ npm install -g @hirey/hi-mcp-server
|
|
|
91
91
|
1. 用宿主把 `hi-mcp-server` 作为 `stdio` MCP server 挂上
|
|
92
92
|
2. 优先执行 `hi_agent_install`,而不是让用户手工走 `register -> connect -> activate`
|
|
93
93
|
3. `hi_agent_install` 会自动完成 register / activate、delivery capability 声明、默认订阅,以及在 OpenClaw 场景下的 receiver 配置
|
|
94
|
-
4. `
|
|
95
|
-
5.
|
|
96
|
-
6.
|
|
94
|
+
4. 当 receiver 的物料配置(例如 hooks token、adapter URL、transport、default reply route)发生变化时,`hi_agent_install` 会先停止当前 profile 记录的 receiver,再写入新配置并按需要重新启动;如果配置没变,则保留现有 runtime cursor,不重置消费进度
|
|
95
|
+
5. `agent_id` 可以省略;省略时由 gateway 正式生成 canonical `ag_...`,不需要用户手写
|
|
96
|
+
6. 完成后执行 `hi_agent_doctor`
|
|
97
|
+
7. 如果要 clean reinstall,先执行 `hi_agent_reset`;`hi_agent_reset` 现在会等待当前 profile 记录的 receiver 真实退出,再删除本地 state / receiver config,避免立刻重装时旧进程把旧配置写回去
|
|
97
98
|
|
|
98
99
|
如果宿主不能直接执行裸命令 `hi-agent-receiver`,而是需要 `node /path/to/dist/server.js`、`npx ...` 或其它显式 argv 前缀,`hi_agent_install` 现在也支持把 receiver 启动入口写成 `receiver_command_argv` 数组;`hi-mcp-server` 会在这组 argv 后自动追加 `run --config <path>`,不再把整段命令误当成单个可执行文件。
|
|
99
100
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type DefaultReplyDeliveryContext = {
|
|
2
|
+
channel: string | null;
|
|
3
|
+
to: string | null;
|
|
4
|
+
account_id: string | null;
|
|
5
|
+
thread_id: string | null;
|
|
6
|
+
};
|
|
7
|
+
export declare function resolveInstallDefaultReplyDeliveryContext(args: {
|
|
8
|
+
hostKind: 'openclaw' | 'generic';
|
|
9
|
+
hasSessionKey: boolean;
|
|
10
|
+
defaultReplyChannel?: unknown;
|
|
11
|
+
defaultReplyTo?: unknown;
|
|
12
|
+
defaultReplyAccountId?: unknown;
|
|
13
|
+
defaultReplyThreadId?: unknown;
|
|
14
|
+
}): DefaultReplyDeliveryContext | null;
|
|
15
|
+
//# sourceMappingURL=defaultReplyRoute.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaultReplyRoute.d.ts","sourceRoot":"","sources":["../src/defaultReplyRoute.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,2BAA2B,GAAG;IACxC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF,wBAAgB,yCAAyC,CAAC,IAAI,EAAE;IAC9D,QAAQ,EAAE,UAAU,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,OAAO,CAAC;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,GAAG,2BAA2B,GAAG,IAAI,CAyBrC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function normalizeText(input) {
|
|
2
|
+
return String(input || '').trim();
|
|
3
|
+
}
|
|
4
|
+
export function resolveInstallDefaultReplyDeliveryContext(args) {
|
|
5
|
+
const explicit = {
|
|
6
|
+
channel: normalizeText(args.defaultReplyChannel) || null,
|
|
7
|
+
to: normalizeText(args.defaultReplyTo) || null,
|
|
8
|
+
account_id: normalizeText(args.defaultReplyAccountId) || null,
|
|
9
|
+
thread_id: normalizeText(args.defaultReplyThreadId) || null,
|
|
10
|
+
};
|
|
11
|
+
const hasExplicit = !!(explicit.channel
|
|
12
|
+
|| explicit.to
|
|
13
|
+
|| explicit.account_id
|
|
14
|
+
|| explicit.thread_id);
|
|
15
|
+
if (hasExplicit)
|
|
16
|
+
return explicit;
|
|
17
|
+
// OpenClaw hooks continuation accepts `last|imessage`; when we already have the
|
|
18
|
+
// canonical session key, `last` is the canonical "same chat" lane.
|
|
19
|
+
if (args.hostKind === 'openclaw' && args.hasSessionKey) {
|
|
20
|
+
return {
|
|
21
|
+
channel: 'last',
|
|
22
|
+
to: null,
|
|
23
|
+
account_id: null,
|
|
24
|
+
thread_id: null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function receiverConfigMaterialSnapshot(raw: unknown): Record<string, unknown> | null;
|
|
2
|
+
export declare function receiverConfigMaterialEquals(left: unknown, right: unknown): boolean;
|
|
3
|
+
export declare function applyReceiverRuntimeSnapshot(nextConfig: Record<string, unknown>, existingConfig: unknown): Record<string, unknown>;
|
|
4
|
+
//# sourceMappingURL=receiver-config-material.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"receiver-config-material.d.ts","sourceRoot":"","sources":["../src/receiver-config-material.ts"],"names":[],"mappings":"AAcA,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAQ3F;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAEnF;AAED,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,cAAc,EAAE,OAAO,GACtB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBzB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function canonicalizeJson(value) {
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return value.map((entry) => canonicalizeJson(entry));
|
|
7
|
+
if (!isPlainObject(value))
|
|
8
|
+
return value;
|
|
9
|
+
const out = {};
|
|
10
|
+
for (const key of Object.keys(value).sort()) {
|
|
11
|
+
out[key] = canonicalizeJson(value[key]);
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
export function receiverConfigMaterialSnapshot(raw) {
|
|
16
|
+
if (!isPlainObject(raw))
|
|
17
|
+
return null;
|
|
18
|
+
const snapshot = JSON.parse(JSON.stringify(raw));
|
|
19
|
+
delete snapshot.runtime;
|
|
20
|
+
delete snapshot._generated_at;
|
|
21
|
+
delete snapshot._generated_by;
|
|
22
|
+
delete snapshot._receiver_config_path;
|
|
23
|
+
return canonicalizeJson(snapshot);
|
|
24
|
+
}
|
|
25
|
+
export function receiverConfigMaterialEquals(left, right) {
|
|
26
|
+
return JSON.stringify(receiverConfigMaterialSnapshot(left)) === JSON.stringify(receiverConfigMaterialSnapshot(right));
|
|
27
|
+
}
|
|
28
|
+
export function applyReceiverRuntimeSnapshot(nextConfig, existingConfig) {
|
|
29
|
+
if (!isPlainObject(existingConfig) || !isPlainObject(existingConfig.runtime))
|
|
30
|
+
return nextConfig;
|
|
31
|
+
const runtime = existingConfig.runtime;
|
|
32
|
+
return {
|
|
33
|
+
...nextConfig,
|
|
34
|
+
// 安装修复时保留已消费 cursor,避免仅因重写物料配置就把 receiver 从头回放。
|
|
35
|
+
runtime: {
|
|
36
|
+
last_consumed_stream_seq: Number.isFinite(Number(runtime.last_consumed_stream_seq))
|
|
37
|
+
? Math.max(0, Number(runtime.last_consumed_stream_seq))
|
|
38
|
+
: 0,
|
|
39
|
+
last_claim_lease_id: typeof runtime.last_claim_lease_id === 'string'
|
|
40
|
+
? runtime.last_claim_lease_id
|
|
41
|
+
: null,
|
|
42
|
+
updated_at: typeof runtime.updated_at === 'string'
|
|
43
|
+
? runtime.updated_at
|
|
44
|
+
: null,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -14,7 +14,11 @@ import { createHiAgentClients, exchangeHiAgentClientCredentialsToken, HiAgentGat
|
|
|
14
14
|
import { readState, resolveCanonicalOpenClawStateDir, resolveDefaultStateDir, resolveLegacyStateFiles, resolveStateFile, updateState, normalizeStateProfile, } from './state.js';
|
|
15
15
|
import { looksLikeOpenClawSessionKey, validateOpenClawSessionKey, } from './openclaw-session-key.js';
|
|
16
16
|
import { buildInstallReceiverCommandArgv, } from './receiver-command.js';
|
|
17
|
+
import { applyReceiverRuntimeSnapshot, receiverConfigMaterialEquals, } from './receiver-config-material.js';
|
|
18
|
+
import { resolveInstallDefaultReplyDeliveryContext, } from './defaultReplyRoute.js';
|
|
17
19
|
const CAPABILITY_CACHE_TTL_MS = 30_000;
|
|
20
|
+
const RECEIVER_STOP_TIMEOUT_MS = 3_000;
|
|
21
|
+
const RECEIVER_STOP_POLL_MS = 100;
|
|
18
22
|
const resolvedProfile = normalizeStateProfile(process.env.HI_MCP_PROFILE);
|
|
19
23
|
const config = {
|
|
20
24
|
host: normalizeText(process.env.HI_MCP_HOST) || '127.0.0.1',
|
|
@@ -622,16 +626,15 @@ function buildDefaultReplyRoute(args, options = {}) {
|
|
|
622
626
|
const sessionKey = normalizeReplyRouteSessionKey(args.host_session_key, {
|
|
623
627
|
requireOpenClawSessionKey: options.requireOpenClawSessionKey,
|
|
624
628
|
});
|
|
625
|
-
const deliveryContext = {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|| deliveryContext.thread_id);
|
|
629
|
+
const deliveryContext = resolveInstallDefaultReplyDeliveryContext({
|
|
630
|
+
hostKind: options.hostKind === 'openclaw' ? 'openclaw' : 'generic',
|
|
631
|
+
hasSessionKey: !!sessionKey,
|
|
632
|
+
defaultReplyChannel: args.default_reply_channel,
|
|
633
|
+
defaultReplyTo: args.default_reply_to,
|
|
634
|
+
defaultReplyAccountId: args.default_reply_account_id,
|
|
635
|
+
defaultReplyThreadId: args.default_reply_thread_id,
|
|
636
|
+
});
|
|
637
|
+
const hasDeliveryContext = !!deliveryContext;
|
|
635
638
|
if (!sessionKey && !hasDeliveryContext)
|
|
636
639
|
return null;
|
|
637
640
|
return {
|
|
@@ -724,6 +727,58 @@ function isProcessAlive(pid) {
|
|
|
724
727
|
return false;
|
|
725
728
|
}
|
|
726
729
|
}
|
|
730
|
+
async function waitForProcessExit(pid, timeoutMs = RECEIVER_STOP_TIMEOUT_MS) {
|
|
731
|
+
const deadline = Date.now() + timeoutMs;
|
|
732
|
+
while (Date.now() < deadline) {
|
|
733
|
+
if (!isProcessAlive(pid))
|
|
734
|
+
return true;
|
|
735
|
+
await sleep(RECEIVER_STOP_POLL_MS);
|
|
736
|
+
}
|
|
737
|
+
return !isProcessAlive(pid);
|
|
738
|
+
}
|
|
739
|
+
async function stopTrackedReceiverProcess(pidRaw) {
|
|
740
|
+
const pid = Number(pidRaw || 0);
|
|
741
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
742
|
+
return {
|
|
743
|
+
attempted: false,
|
|
744
|
+
pid: null,
|
|
745
|
+
signal: null,
|
|
746
|
+
exited: true,
|
|
747
|
+
timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
if (!isProcessAlive(pid)) {
|
|
751
|
+
return {
|
|
752
|
+
attempted: true,
|
|
753
|
+
pid,
|
|
754
|
+
signal: null,
|
|
755
|
+
exited: true,
|
|
756
|
+
timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
process.kill(pid, 'SIGTERM');
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
if (error?.code === 'ESRCH') {
|
|
764
|
+
return {
|
|
765
|
+
attempted: true,
|
|
766
|
+
pid,
|
|
767
|
+
signal: 'SIGTERM',
|
|
768
|
+
exited: true,
|
|
769
|
+
timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
attempted: true,
|
|
776
|
+
pid,
|
|
777
|
+
signal: 'SIGTERM',
|
|
778
|
+
exited: await waitForProcessExit(pid),
|
|
779
|
+
timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
727
782
|
async function startDetachedReceiver(args) {
|
|
728
783
|
const [command, ...prefixArgs] = args.receiverCommandArgv;
|
|
729
784
|
if (!command)
|
|
@@ -1204,6 +1259,7 @@ async function handleInstall(args) {
|
|
|
1204
1259
|
const defaultReplyRoute = buildDefaultReplyRoute(args, {
|
|
1205
1260
|
installationId: state.identity?.installation_id || null,
|
|
1206
1261
|
requireOpenClawSessionKey: hostKind === 'openclaw',
|
|
1262
|
+
hostKind,
|
|
1207
1263
|
});
|
|
1208
1264
|
const routeMissingPolicy = normalizeText(args.route_missing_policy)
|
|
1209
1265
|
|| (defaultReplyRoute ? 'use_explicit_default_route' : '');
|
|
@@ -1241,7 +1297,8 @@ async function handleInstall(args) {
|
|
|
1241
1297
|
return fail('missing_host_adapter_url');
|
|
1242
1298
|
}
|
|
1243
1299
|
const receiverConfigPath = resolveReceiverConfigPath(await loadPersistedState());
|
|
1244
|
-
|
|
1300
|
+
let existingReceiverConfig = await readReceiverConfigSnapshot(receiverConfigPath);
|
|
1301
|
+
const desiredReceiverConfig = buildReceiverConfig({
|
|
1245
1302
|
receiverConfigPath,
|
|
1246
1303
|
hostKind,
|
|
1247
1304
|
receiverTransport,
|
|
@@ -1251,16 +1308,37 @@ async function handleInstall(args) {
|
|
|
1251
1308
|
openresponsesModel: normalizeText(args.openresponses_model) || undefined,
|
|
1252
1309
|
defaultReplyRoute,
|
|
1253
1310
|
});
|
|
1254
|
-
|
|
1311
|
+
const receiverConfigChanged = !receiverConfigMaterialEquals(existingReceiverConfig, desiredReceiverConfig);
|
|
1312
|
+
let receiverPid = stateInstallSnapshot(state.runtime).receiver_pid;
|
|
1313
|
+
let receiverStop = null;
|
|
1314
|
+
if (receiverConfigChanged && isProcessAlive(receiverPid)) {
|
|
1315
|
+
// 先停 tracked receiver,避免旧进程继续拿旧 token 投递,并把旧物料配置再写回磁盘。
|
|
1316
|
+
receiverStop = await stopTrackedReceiverProcess(receiverPid);
|
|
1317
|
+
if (!normalizeBooleanFlag(receiverStop?.exited)) {
|
|
1318
|
+
return fail('receiver_stop_timeout', {
|
|
1319
|
+
receiver_config_path: receiverConfigPath,
|
|
1320
|
+
receiver_pid: receiverPid,
|
|
1321
|
+
receiver_stop: receiverStop,
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
receiverPid = null;
|
|
1325
|
+
// 停掉 tracked receiver 后再读一次磁盘,尽量保留它退出前最后一次写下的 runtime cursor。
|
|
1326
|
+
existingReceiverConfig = await readReceiverConfigSnapshot(receiverConfigPath);
|
|
1327
|
+
}
|
|
1328
|
+
if (receiverConfigChanged) {
|
|
1329
|
+
const receiverConfig = applyReceiverRuntimeSnapshot(desiredReceiverConfig, existingReceiverConfig);
|
|
1330
|
+
await writeReceiverConfigFile(receiverConfigPath, receiverConfig);
|
|
1331
|
+
}
|
|
1255
1332
|
state = await persistState((current) => ({
|
|
1256
1333
|
...current,
|
|
1257
1334
|
runtime: buildInstallRuntimeState(current.runtime, {
|
|
1258
1335
|
host_kind: hostKind,
|
|
1259
1336
|
receiver_config_path: receiverConfigPath,
|
|
1337
|
+
receiver_pid: isProcessAlive(receiverPid) ? receiverPid : null,
|
|
1260
1338
|
receiver_last_error: null,
|
|
1261
1339
|
}),
|
|
1262
1340
|
}));
|
|
1263
|
-
|
|
1341
|
+
receiverPid = stateInstallSnapshot(state.runtime).receiver_pid;
|
|
1264
1342
|
if (receiverShouldStart && !isProcessAlive(receiverPid)) {
|
|
1265
1343
|
try {
|
|
1266
1344
|
receiverPid = await startDetachedReceiver({
|
|
@@ -1296,12 +1374,14 @@ async function handleInstall(args) {
|
|
|
1296
1374
|
}
|
|
1297
1375
|
receiverPayload = {
|
|
1298
1376
|
config_path: receiverConfigPath,
|
|
1377
|
+
config_changed: receiverConfigChanged,
|
|
1299
1378
|
started: receiverShouldStart,
|
|
1300
1379
|
receiver_pid: receiverPid || null,
|
|
1301
1380
|
receiver_command_argv: receiverCommandArgv,
|
|
1302
1381
|
transport: receiverTransport,
|
|
1303
1382
|
host_adapter_kind: hostAdapterKind,
|
|
1304
1383
|
host_adapter_url: hostAdapterUrl,
|
|
1384
|
+
...(receiverStop ? { receiver_stop: receiverStop } : {}),
|
|
1305
1385
|
};
|
|
1306
1386
|
}
|
|
1307
1387
|
if (receiverShouldStart) {
|
|
@@ -1334,22 +1414,21 @@ async function handleReset(args) {
|
|
|
1334
1414
|
const removeReceiverConfig = args.remove_receiver_config !== false;
|
|
1335
1415
|
const clearState = args.clear_state !== false;
|
|
1336
1416
|
let stopResult = null;
|
|
1337
|
-
if (stopReceiver &&
|
|
1417
|
+
if (stopReceiver && Number.isInteger(installState.receiver_pid) && Number(installState.receiver_pid) > 0) {
|
|
1338
1418
|
try {
|
|
1339
|
-
|
|
1340
|
-
stopResult = {
|
|
1341
|
-
attempted: true,
|
|
1342
|
-
pid: installState.receiver_pid,
|
|
1343
|
-
signalled: true,
|
|
1344
|
-
};
|
|
1419
|
+
stopResult = await stopTrackedReceiverProcess(installState.receiver_pid);
|
|
1345
1420
|
}
|
|
1346
1421
|
catch (error) {
|
|
1347
|
-
|
|
1348
|
-
attempted: true,
|
|
1422
|
+
return fail('receiver_stop_failed', {
|
|
1349
1423
|
pid: installState.receiver_pid,
|
|
1350
|
-
signalled: false,
|
|
1351
1424
|
error: String(error?.message || error || 'receiver_stop_failed'),
|
|
1352
|
-
};
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
if (!normalizeBooleanFlag(stopResult?.exited)) {
|
|
1428
|
+
return fail('receiver_stop_timeout', {
|
|
1429
|
+
pid: installState.receiver_pid,
|
|
1430
|
+
receiver_stop: stopResult,
|
|
1431
|
+
});
|
|
1353
1432
|
}
|
|
1354
1433
|
}
|
|
1355
1434
|
const receiverConfigPath = installState.receiver_config_path || resolveReceiverConfigPath(state);
|