agent-remnote 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +47 -0
- package/dist/main.js +7969 -4691
- package/package.json +19 -4
- package/dist/apps/cli/src/adapters/mcp.js +0 -1
- package/dist/apps/cli/src/commands/_enqueue.js +0 -138
- package/dist/apps/cli/src/commands/_shared.js +0 -57
- package/dist/apps/cli/src/commands/_tool.js +0 -28
- package/dist/apps/cli/src/commands/apply.js +0 -81
- package/dist/apps/cli/src/commands/config/index.js +0 -3
- package/dist/apps/cli/src/commands/config/print.js +0 -28
- package/dist/apps/cli/src/commands/daily/index.js +0 -4
- package/dist/apps/cli/src/commands/daily/summary.js +0 -25
- package/dist/apps/cli/src/commands/daily/write.js +0 -145
- package/dist/apps/cli/src/commands/db/backups.js +0 -23
- package/dist/apps/cli/src/commands/db/index.js +0 -4
- package/dist/apps/cli/src/commands/db/recent.js +0 -178
- package/dist/apps/cli/src/commands/doctor.js +0 -124
- package/dist/apps/cli/src/commands/index.js +0 -73
- package/dist/apps/cli/src/commands/ops/index.js +0 -4
- package/dist/apps/cli/src/commands/ops/list.js +0 -12
- package/dist/apps/cli/src/commands/ops/schema.js +0 -77
- package/dist/apps/cli/src/commands/queue/enqueue.js +0 -73
- package/dist/apps/cli/src/commands/queue/index.js +0 -5
- package/dist/apps/cli/src/commands/queue/inspect.js +0 -26
- package/dist/apps/cli/src/commands/queue/stats.js +0 -14
- package/dist/apps/cli/src/commands/read/by-reference.js +0 -35
- package/dist/apps/cli/src/commands/read/connections.js +0 -15
- package/dist/apps/cli/src/commands/read/index.js +0 -21
- package/dist/apps/cli/src/commands/read/inspect.js +0 -34
- package/dist/apps/cli/src/commands/read/outline.js +0 -59
- package/dist/apps/cli/src/commands/read/query.js +0 -95
- package/dist/apps/cli/src/commands/read/references.js +0 -41
- package/dist/apps/cli/src/commands/read/resolve-ref.js +0 -32
- package/dist/apps/cli/src/commands/read/search.js +0 -40
- package/dist/apps/cli/src/commands/read/table.js +0 -32
- package/dist/apps/cli/src/commands/todos/index.js +0 -3
- package/dist/apps/cli/src/commands/todos/list.js +0 -33
- package/dist/apps/cli/src/commands/topic/index.js +0 -3
- package/dist/apps/cli/src/commands/topic/summary.js +0 -44
- package/dist/apps/cli/src/commands/wechat/index.js +0 -3
- package/dist/apps/cli/src/commands/wechat/outline.js +0 -430
- package/dist/apps/cli/src/commands/write/bullet.js +0 -76
- package/dist/apps/cli/src/commands/write/index.js +0 -4
- package/dist/apps/cli/src/commands/write/md.js +0 -91
- package/dist/apps/cli/src/commands/ws/_shared.js +0 -129
- package/dist/apps/cli/src/commands/ws/ensure.js +0 -22
- package/dist/apps/cli/src/commands/ws/health.js +0 -15
- package/dist/apps/cli/src/commands/ws/index.js +0 -21
- package/dist/apps/cli/src/commands/ws/logs.js +0 -95
- package/dist/apps/cli/src/commands/ws/restart.js +0 -73
- package/dist/apps/cli/src/commands/ws/serve.js +0 -52
- package/dist/apps/cli/src/commands/ws/start.js +0 -70
- package/dist/apps/cli/src/commands/ws/status.js +0 -60
- package/dist/apps/cli/src/commands/ws/stop.js +0 -59
- package/dist/apps/cli/src/commands/ws/trigger.js +0 -20
- package/dist/apps/cli/src/main.js +0 -79
- package/dist/apps/cli/src/services/AppConfig.js +0 -3
- package/dist/apps/cli/src/services/Config.js +0 -91
- package/dist/apps/cli/src/services/DaemonFiles.js +0 -91
- package/dist/apps/cli/src/services/Errors.js +0 -49
- package/dist/apps/cli/src/services/Output.js +0 -16
- package/dist/apps/cli/src/services/Payload.js +0 -90
- package/dist/apps/cli/src/services/Process.js +0 -94
- package/dist/apps/cli/src/services/Queue.js +0 -120
- package/dist/apps/cli/src/services/RefResolver.js +0 -111
- package/dist/apps/cli/src/services/RemDb.js +0 -35
- package/dist/apps/cli/src/services/WsClient.js +0 -170
- package/dist/apps/cli/tests/apply.contract.test.js +0 -31
- package/dist/apps/cli/tests/db-recent.contract.test.js +0 -22
- package/dist/apps/cli/tests/help.contract.test.js +0 -30
- package/dist/apps/cli/tests/helpers/runCli.js +0 -45
- package/dist/apps/cli/tests/ids-output.contract.test.js +0 -30
- package/dist/apps/cli/tests/payload-stdin.contract.test.js +0 -15
- package/dist/apps/cli/tests/read-search.contract.test.js +0 -22
- package/dist/apps/cli/tests/ws-health.contract.test.js +0 -36
- package/dist/apps/cli/vitest.config.js +0 -7
- package/dist/packages/mcp/src/public.js +0 -18
- package/dist/packages/mcp/src/queue/dao.js +0 -165
- package/dist/packages/mcp/src/queue/db.js +0 -26
- package/dist/packages/mcp/src/tools/executeSearchQuery.js +0 -914
- package/dist/packages/mcp/src/tools/findRemsByReference.js +0 -447
- package/dist/packages/mcp/src/tools/getRemConnections.js +0 -566
- package/dist/packages/mcp/src/tools/inspectRemDoc.js +0 -60
- package/dist/packages/mcp/src/tools/listRemBackups.js +0 -35
- package/dist/packages/mcp/src/tools/listRemReferences.js +0 -421
- package/dist/packages/mcp/src/tools/listSupportedOps.js +0 -41
- package/dist/packages/mcp/src/tools/listTodos.js +0 -815
- package/dist/packages/mcp/src/tools/outlineRemSubtree.js +0 -203
- package/dist/packages/mcp/src/tools/readRemTable.js +0 -252
- package/dist/packages/mcp/src/tools/resolveRemReference.js +0 -174
- package/dist/packages/mcp/src/tools/searchQueryTypes.js +0 -127
- package/dist/packages/mcp/src/tools/searchRemOverview.js +0 -422
- package/dist/packages/mcp/src/tools/searchUtils.js +0 -32
- package/dist/packages/mcp/src/tools/shared.js +0 -393
- package/dist/packages/mcp/src/tools/summarizeDailyNotes.js +0 -221
- package/dist/packages/mcp/src/tools/summarizeTopicActivity.js +0 -605
- package/dist/packages/mcp/src/tools/timeFilters.js +0 -130
- package/dist/packages/mcp/src/ws/bridge.js +0 -377
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
export const TIME_RANGE_PATTERN = /^(\d+)\s*([hdwmy])$/i;
|
|
3
|
-
// 允许将 "all" 或 "*" 视为无时间限制(即不应用任何时间过滤)
|
|
4
|
-
const ALL_TIME_RANGE_TOKENS = new Set(["all", "*"]);
|
|
5
|
-
export const timeValueSchema = z.union([z.number().finite(), z.string().min(1)]);
|
|
6
|
-
export function resolveTimeFilters(input, options) {
|
|
7
|
-
const filters = {};
|
|
8
|
-
const summary = {};
|
|
9
|
-
// 确保在所有分支中都有定义,避免引用错误
|
|
10
|
-
let effectiveTimeRange;
|
|
11
|
-
const explicitProvided = input.timeRange !== undefined ||
|
|
12
|
-
input.createdAfter !== undefined ||
|
|
13
|
-
input.createdBefore !== undefined ||
|
|
14
|
-
input.updatedAfter !== undefined ||
|
|
15
|
-
input.updatedBefore !== undefined;
|
|
16
|
-
// 规范化 timeRange 字符串
|
|
17
|
-
const normalizedTimeRange = typeof input.timeRange === "string" ? input.timeRange.trim().toLowerCase() : undefined;
|
|
18
|
-
// 如果显式传入 all/*,表示无时间限制:不应用任何时间过滤条件
|
|
19
|
-
if (normalizedTimeRange && ALL_TIME_RANGE_TOKENS.has(normalizedTimeRange)) {
|
|
20
|
-
summary.timeRange = input.timeRange ?? "all";
|
|
21
|
-
// 直接跳过默认 timeRange 与阈值计算
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
effectiveTimeRange =
|
|
25
|
-
input.timeRange ?? (!explicitProvided ? options?.defaultTimeRange : undefined);
|
|
26
|
-
if (effectiveTimeRange) {
|
|
27
|
-
const durationMs = parseTimeRange(effectiveTimeRange);
|
|
28
|
-
if (durationMs == null) {
|
|
29
|
-
throw new Error("timeRange 解析失败,需形如 '30d'、'2w'、'12h'");
|
|
30
|
-
}
|
|
31
|
-
const threshold = Date.now() - durationMs;
|
|
32
|
-
if (input.updatedAfter === undefined) {
|
|
33
|
-
filters.updatedAfter = threshold;
|
|
34
|
-
summary.updatedAfter = describeTimestamp(threshold);
|
|
35
|
-
}
|
|
36
|
-
if (input.createdAfter === undefined) {
|
|
37
|
-
filters.createdAfter = threshold;
|
|
38
|
-
summary.createdAfter = describeTimestamp(threshold);
|
|
39
|
-
}
|
|
40
|
-
summary.timeRange = effectiveTimeRange;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
const explicitMappings = [
|
|
44
|
-
{ key: "createdAfter", value: input.createdAfter, summaryKey: "createdAfter" },
|
|
45
|
-
{ key: "createdBefore", value: input.createdBefore, summaryKey: "createdBefore" },
|
|
46
|
-
{ key: "updatedAfter", value: input.updatedAfter, summaryKey: "updatedAfter" },
|
|
47
|
-
{ key: "updatedBefore", value: input.updatedBefore, summaryKey: "updatedBefore" },
|
|
48
|
-
];
|
|
49
|
-
for (const entry of explicitMappings) {
|
|
50
|
-
if (entry.value === undefined || entry.value === null)
|
|
51
|
-
continue;
|
|
52
|
-
const parsed = parseTemporalInput(entry.value);
|
|
53
|
-
if (parsed == null) {
|
|
54
|
-
throw new Error(`${entry.summaryKey} 解析失败,需要传入毫秒时间戳或 ISO 日期字符串`);
|
|
55
|
-
}
|
|
56
|
-
;
|
|
57
|
-
filters[entry.key] = parsed;
|
|
58
|
-
summary[entry.summaryKey] = describeTimestamp(parsed);
|
|
59
|
-
}
|
|
60
|
-
return { filters, summary, effectiveTimeRange };
|
|
61
|
-
}
|
|
62
|
-
export function parseTemporalInput(value) {
|
|
63
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
64
|
-
return Math.trunc(value);
|
|
65
|
-
}
|
|
66
|
-
if (typeof value === "string") {
|
|
67
|
-
const trimmed = value.trim();
|
|
68
|
-
if (!trimmed)
|
|
69
|
-
return null;
|
|
70
|
-
if (/^-?\d+$/.test(trimmed)) {
|
|
71
|
-
const num = Number(trimmed);
|
|
72
|
-
return Number.isFinite(num) ? Math.trunc(num) : null;
|
|
73
|
-
}
|
|
74
|
-
const parsed = Date.parse(trimmed);
|
|
75
|
-
if (Number.isNaN(parsed)) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
return parsed;
|
|
79
|
-
}
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
export function parseTimeRange(input) {
|
|
83
|
-
const match = input.match(TIME_RANGE_PATTERN);
|
|
84
|
-
if (!match)
|
|
85
|
-
return null;
|
|
86
|
-
const value = Number(match[1]);
|
|
87
|
-
if (!Number.isFinite(value))
|
|
88
|
-
return null;
|
|
89
|
-
const unit = match[2].toLowerCase();
|
|
90
|
-
const base = {
|
|
91
|
-
h: 60 * 60 * 1000,
|
|
92
|
-
d: 24 * 60 * 60 * 1000,
|
|
93
|
-
w: 7 * 24 * 60 * 60 * 1000,
|
|
94
|
-
m: 30 * 24 * 60 * 60 * 1000,
|
|
95
|
-
y: 365 * 24 * 60 * 60 * 1000,
|
|
96
|
-
};
|
|
97
|
-
const multiplier = base[unit];
|
|
98
|
-
if (!multiplier)
|
|
99
|
-
return null;
|
|
100
|
-
return value * multiplier;
|
|
101
|
-
}
|
|
102
|
-
export function describeTimestamp(ms) {
|
|
103
|
-
return {
|
|
104
|
-
ms,
|
|
105
|
-
iso: new Date(ms).toISOString(),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
export function describeFilterSummary(summary) {
|
|
109
|
-
if (!summary)
|
|
110
|
-
return null;
|
|
111
|
-
const parts = [];
|
|
112
|
-
if (summary.timeRange) {
|
|
113
|
-
parts.push(`timeRange=${summary.timeRange}`);
|
|
114
|
-
}
|
|
115
|
-
if (summary.updatedAfter) {
|
|
116
|
-
parts.push(`updated ≥ ${summary.updatedAfter.iso}`);
|
|
117
|
-
}
|
|
118
|
-
if (summary.updatedBefore) {
|
|
119
|
-
parts.push(`updated ≤ ${summary.updatedBefore.iso}`);
|
|
120
|
-
}
|
|
121
|
-
if (summary.createdAfter) {
|
|
122
|
-
parts.push(`created ≥ ${summary.createdAfter.iso}`);
|
|
123
|
-
}
|
|
124
|
-
if (summary.createdBefore) {
|
|
125
|
-
parts.push(`created ≤ ${summary.createdBefore.iso}`);
|
|
126
|
-
}
|
|
127
|
-
if (parts.length === 0)
|
|
128
|
-
return null;
|
|
129
|
-
return parts.join(",");
|
|
130
|
-
}
|
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
import { WebSocketServer } from "ws";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { openQueueDb } from "../queue/db.js";
|
|
5
|
-
import { ackDead, ackRetry, ackSuccess, claimNextOp, getTxnIdByOpId, queueStats, recoverExpiredLeases, upsertIdMap } from "../queue/dao.js";
|
|
6
|
-
const clientMeta = new Map();
|
|
7
|
-
const GLOBAL_BRIDGE_KEY = Symbol.for("__REMNOTE_WS_BRIDGE__");
|
|
8
|
-
const globalAny = globalThis;
|
|
9
|
-
function setStoredBridge(bridge) {
|
|
10
|
-
if (bridge) {
|
|
11
|
-
globalAny[GLOBAL_BRIDGE_KEY] = bridge;
|
|
12
|
-
}
|
|
13
|
-
else {
|
|
14
|
-
delete globalAny[GLOBAL_BRIDGE_KEY];
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
function isWssListening(wss) {
|
|
18
|
-
if (!wss)
|
|
19
|
-
return false;
|
|
20
|
-
try {
|
|
21
|
-
const addr = wss.address();
|
|
22
|
-
if (!addr)
|
|
23
|
-
return false;
|
|
24
|
-
if (typeof addr === "string")
|
|
25
|
-
return addr.length > 0;
|
|
26
|
-
if (typeof addr.port === "number")
|
|
27
|
-
return true;
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
catch (_) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function getStoredBridge() {
|
|
35
|
-
const existing = globalAny[GLOBAL_BRIDGE_KEY];
|
|
36
|
-
if (!existing)
|
|
37
|
-
return undefined;
|
|
38
|
-
if (!isWssListening(existing.wss)) {
|
|
39
|
-
setStoredBridge(undefined);
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
return existing;
|
|
43
|
-
}
|
|
44
|
-
export function getWsStatus() {
|
|
45
|
-
const rows = [];
|
|
46
|
-
for (const ws of clientMeta.keys()) {
|
|
47
|
-
const meta = clientMeta.get(ws);
|
|
48
|
-
rows.push({
|
|
49
|
-
consumerId: meta.consumerId,
|
|
50
|
-
connectedAt: meta.connectedAt,
|
|
51
|
-
lastSeenAt: meta.lastSeenAt,
|
|
52
|
-
remoteAddr: meta.remoteAddr,
|
|
53
|
-
readyState: ws.readyState,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return { clients: rows };
|
|
57
|
-
}
|
|
58
|
-
export function notifyStartSync(targetConsumerId) {
|
|
59
|
-
let sent = 0;
|
|
60
|
-
for (const [ws, meta] of clientMeta.entries()) {
|
|
61
|
-
if (targetConsumerId && meta.consumerId !== targetConsumerId)
|
|
62
|
-
continue;
|
|
63
|
-
try {
|
|
64
|
-
ws.send(JSON.stringify({ type: "StartSync" }));
|
|
65
|
-
if (DEBUG)
|
|
66
|
-
log("StartSync sent", { to: meta.consumerId, remote: meta.remoteAddr });
|
|
67
|
-
sent += 1;
|
|
68
|
-
}
|
|
69
|
-
catch (_) { }
|
|
70
|
-
}
|
|
71
|
-
return { sent };
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* 启动最小可用的 WebSocket 桥接服务。
|
|
75
|
-
* - 默认端口:从环境变量 `REMNOTE_WS_PORT`、`WS_PORT`,否则 3010
|
|
76
|
-
* - 默认路径:`/ws`
|
|
77
|
-
* - 支持心跳保活,客户端可按需发送/响应 `ping/pong`
|
|
78
|
-
*/
|
|
79
|
-
export function startWebSocketBridge(opts = {}) {
|
|
80
|
-
const enabled = opts.enable ?? envEnabled();
|
|
81
|
-
if (!enabled)
|
|
82
|
-
return undefined;
|
|
83
|
-
const port = opts.port ?? envPort();
|
|
84
|
-
const path = opts.path ?? process.env.REMNOTE_WS_PATH ?? "/ws";
|
|
85
|
-
const heartbeatIntervalMs = opts.heartbeatIntervalMs ?? 30_000;
|
|
86
|
-
// 单进程热更新守卫:若已启动则直接复用,避免重复绑定端口
|
|
87
|
-
const existing = getStoredBridge();
|
|
88
|
-
if (existing)
|
|
89
|
-
return existing;
|
|
90
|
-
let wss;
|
|
91
|
-
try {
|
|
92
|
-
wss = new WebSocketServer({ port, path });
|
|
93
|
-
}
|
|
94
|
-
catch (e) {
|
|
95
|
-
const msg = String(e?.message || e || "");
|
|
96
|
-
if (msg.includes("EADDRINUSE")) {
|
|
97
|
-
// 端口占用常见于 watch 重启时旧进程尚未完全释放;此处仅提示并返回。
|
|
98
|
-
// 建议配合进程退出时的 close() 钩子与单进程守卫,减少该情况出现概率。
|
|
99
|
-
console.error(`ws port already in use (${port}${path}); likely due to hot reload overlap.`);
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
console.error(`failed to start websocket bridge on port ${port}${path}:`, msg);
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
const db = openQueueDb();
|
|
106
|
-
if (DEBUG)
|
|
107
|
-
log("WS bridge started", { port, path });
|
|
108
|
-
wss.on("listening", () => {
|
|
109
|
-
console.log(`websocket bridge ready at ws://localhost:${port}${path}`);
|
|
110
|
-
});
|
|
111
|
-
wss.on("connection", (ws, req) => {
|
|
112
|
-
;
|
|
113
|
-
ws.isAlive = true;
|
|
114
|
-
const now = Date.now();
|
|
115
|
-
const remoteAddr = req.socket.remoteAddress;
|
|
116
|
-
const userAgent = req.headers["user-agent"];
|
|
117
|
-
clientMeta.set(ws, { connectedAt: now, lastSeenAt: now, remoteAddr, userAgent });
|
|
118
|
-
if (DEBUG)
|
|
119
|
-
log("connection", { remoteAddr, userAgent });
|
|
120
|
-
ws.on("pong", () => {
|
|
121
|
-
;
|
|
122
|
-
ws.isAlive = true;
|
|
123
|
-
const meta = clientMeta.get(ws);
|
|
124
|
-
if (meta)
|
|
125
|
-
meta.lastSeenAt = Date.now();
|
|
126
|
-
});
|
|
127
|
-
ws.on("message", (raw) => {
|
|
128
|
-
try {
|
|
129
|
-
const txt = raw.toString();
|
|
130
|
-
if (txt === "ping") {
|
|
131
|
-
ws.send("pong");
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
const msg = JSON.parse(txt);
|
|
135
|
-
const meta = clientMeta.get(ws);
|
|
136
|
-
if (meta)
|
|
137
|
-
meta.lastSeenAt = Date.now();
|
|
138
|
-
if (DEBUG)
|
|
139
|
-
log("message", { from: meta?.consumerId, type: msg?.type });
|
|
140
|
-
handleMessage(ws, db, msg, ws);
|
|
141
|
-
}
|
|
142
|
-
catch (e) {
|
|
143
|
-
ws.send(JSON.stringify({ type: "error", message: e.message }));
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
ws.on("close", () => {
|
|
147
|
-
// 连接关闭时清理
|
|
148
|
-
clientMeta.delete(ws);
|
|
149
|
-
if (DEBUG)
|
|
150
|
-
log("close");
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
// 心跳保活,剔除断开连接的客户端
|
|
154
|
-
const interval = setInterval(() => {
|
|
155
|
-
wss.clients.forEach((ws) => {
|
|
156
|
-
const sock = ws;
|
|
157
|
-
if (sock.isAlive === false)
|
|
158
|
-
return ws.terminate();
|
|
159
|
-
sock.isAlive = false;
|
|
160
|
-
ws.ping();
|
|
161
|
-
});
|
|
162
|
-
// 回收过期租约,避免任务卡死
|
|
163
|
-
try {
|
|
164
|
-
const n = recoverExpiredLeases(db);
|
|
165
|
-
if (DEBUG && n > 0)
|
|
166
|
-
log("lease_recovered", { count: n });
|
|
167
|
-
}
|
|
168
|
-
catch (_) { }
|
|
169
|
-
}, heartbeatIntervalMs);
|
|
170
|
-
const started = {
|
|
171
|
-
wss,
|
|
172
|
-
close: async () => {
|
|
173
|
-
clearInterval(interval);
|
|
174
|
-
await new Promise((resolve) => wss.close(() => resolve()));
|
|
175
|
-
if (globalAny[GLOBAL_BRIDGE_KEY] === started) {
|
|
176
|
-
setStoredBridge(undefined);
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
|
-
// 记录到 global,避免单进程内的二次初始化
|
|
181
|
-
setStoredBridge(started);
|
|
182
|
-
// 监听退出信号,尽快释放端口,减少 watch 重启时的占用窗口
|
|
183
|
-
const onExit = async () => {
|
|
184
|
-
try {
|
|
185
|
-
await started.close();
|
|
186
|
-
}
|
|
187
|
-
catch (_) { }
|
|
188
|
-
};
|
|
189
|
-
try {
|
|
190
|
-
process.once("SIGINT", onExit);
|
|
191
|
-
process.once("SIGTERM", onExit);
|
|
192
|
-
process.once("exit", onExit);
|
|
193
|
-
}
|
|
194
|
-
catch (_) { }
|
|
195
|
-
return started;
|
|
196
|
-
}
|
|
197
|
-
export function ensureWebSocketBridge(opts = {}) {
|
|
198
|
-
const existing = getStoredBridge();
|
|
199
|
-
if (existing)
|
|
200
|
-
return { bridge: existing, restarted: false };
|
|
201
|
-
const bridge = startWebSocketBridge({ ...opts, enable: opts.enable ?? true });
|
|
202
|
-
return { bridge, restarted: !!bridge };
|
|
203
|
-
}
|
|
204
|
-
function envPort() {
|
|
205
|
-
const raw = process.env.REMNOTE_WS_PORT || process.env.WS_PORT;
|
|
206
|
-
const n = Number(raw);
|
|
207
|
-
return Number.isFinite(n) && n > 0 ? n : 3010;
|
|
208
|
-
}
|
|
209
|
-
function envEnabled() {
|
|
210
|
-
const flag = (process.env.REMNOTE_WS_ENABLED || process.env.WS_ENABLED || "").toLowerCase();
|
|
211
|
-
const disabled = (process.env.REMNOTE_WS_DISABLED || process.env.NO_WS || "").toLowerCase();
|
|
212
|
-
if (disabled === "1" || disabled === "true")
|
|
213
|
-
return false;
|
|
214
|
-
if (flag === "0" || flag === "false")
|
|
215
|
-
return false;
|
|
216
|
-
// 默认开启
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
function handleMessage(ws, db, msg, sock) {
|
|
220
|
-
const send = (obj) => ws.send(JSON.stringify(obj));
|
|
221
|
-
switch (msg?.type) {
|
|
222
|
-
case "Hello": {
|
|
223
|
-
// 暂不强制鉴权,保留占位
|
|
224
|
-
send({ type: "HelloAck", ok: true });
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
case "Register": {
|
|
228
|
-
const consumerId = String(msg.consumerId || "") || undefined;
|
|
229
|
-
const meta = clientMeta.get(sock);
|
|
230
|
-
if (meta)
|
|
231
|
-
meta.consumerId = consumerId;
|
|
232
|
-
if (DEBUG)
|
|
233
|
-
log("registered", { consumerId });
|
|
234
|
-
return send({ type: "Registered", consumerId });
|
|
235
|
-
}
|
|
236
|
-
case "RequestOp": {
|
|
237
|
-
const consumer = msg.consumerId || "plugin";
|
|
238
|
-
const meta = clientMeta.get(sock);
|
|
239
|
-
if (meta)
|
|
240
|
-
meta.consumerId = consumer;
|
|
241
|
-
const leaseMs = typeof msg.leaseMs === "number" ? msg.leaseMs : 30000;
|
|
242
|
-
const op = claimNextOp(db, consumer, leaseMs);
|
|
243
|
-
if (!op) {
|
|
244
|
-
if (DEBUG)
|
|
245
|
-
log("no_work", { consumer });
|
|
246
|
-
return send({ type: "NoWork" });
|
|
247
|
-
}
|
|
248
|
-
if (DEBUG)
|
|
249
|
-
log("dispatch", { op_id: op.op_id, txn_id: op.txn_id, type: op.type, consumer });
|
|
250
|
-
return send({
|
|
251
|
-
type: "OpDispatch",
|
|
252
|
-
op_id: op.op_id,
|
|
253
|
-
txn_id: op.txn_id,
|
|
254
|
-
op_seq: op.op_seq,
|
|
255
|
-
op_type: op.type,
|
|
256
|
-
payload: safeParseJson(op.payload_json),
|
|
257
|
-
idempotency_key: op.idempotency_key,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
case "TriggerStartSync": {
|
|
261
|
-
// 开发用途:收到管理指令后向所有或指定 consumerId 广播 StartSync
|
|
262
|
-
const target = typeof msg?.consumerId === 'string' && msg.consumerId.trim() ? String(msg.consumerId) : undefined;
|
|
263
|
-
const res = notifyStartSync(target);
|
|
264
|
-
if (DEBUG)
|
|
265
|
-
log("trigger_start_sync", { target, sent: res.sent });
|
|
266
|
-
return send({ type: "StartSyncTriggered", sent: res.sent });
|
|
267
|
-
}
|
|
268
|
-
case "OpAck": {
|
|
269
|
-
const { op_id, status } = msg;
|
|
270
|
-
if (!op_id || !status)
|
|
271
|
-
return send({ type: "Error", message: "invalid OpAck" });
|
|
272
|
-
if (status === "success") {
|
|
273
|
-
ackSuccess(db, op_id, msg.result || null);
|
|
274
|
-
// 可选的 ID 映射回填
|
|
275
|
-
try {
|
|
276
|
-
const mappings = [];
|
|
277
|
-
const created = msg?.result?.created;
|
|
278
|
-
if (created?.client_temp_id && created?.remote_id)
|
|
279
|
-
mappings.push({ client_temp_id: created.client_temp_id, remote_id: created.remote_id, remote_type: created.remote_type || 'rem' });
|
|
280
|
-
if (Array.isArray(msg?.result?.id_map)) {
|
|
281
|
-
for (const m of msg.result.id_map)
|
|
282
|
-
if (m?.client_temp_id && m?.remote_id)
|
|
283
|
-
mappings.push({ client_temp_id: m.client_temp_id, remote_id: m.remote_id, remote_type: m.remote_type || 'rem' });
|
|
284
|
-
}
|
|
285
|
-
if (Array.isArray(msg?.remote_id_map)) {
|
|
286
|
-
for (const m of msg.remote_id_map)
|
|
287
|
-
if (m?.client_temp_id && m?.remote_id)
|
|
288
|
-
mappings.push({ client_temp_id: m.client_temp_id, remote_id: m.remote_id, remote_type: m.remote_type || 'rem' });
|
|
289
|
-
}
|
|
290
|
-
if (mappings.length > 0) {
|
|
291
|
-
const txn_id = getTxnIdByOpId(db, op_id);
|
|
292
|
-
upsertIdMap(db, mappings.map((m) => ({ ...m, source_txn: txn_id })));
|
|
293
|
-
}
|
|
294
|
-
if (DEBUG)
|
|
295
|
-
log("ack_success", { op_id, mappings: msg?.result?.created ? 1 : (Array.isArray(msg?.result?.id_map) ? msg.result.id_map.length : 0) });
|
|
296
|
-
}
|
|
297
|
-
catch (_) { }
|
|
298
|
-
return send({ type: "AckOk", op_id });
|
|
299
|
-
}
|
|
300
|
-
else if (status === "retry") {
|
|
301
|
-
ackRetry(db, op_id, { code: msg.error_code, message: msg.error_message, retryAfterMs: msg.retry_after_ms });
|
|
302
|
-
if (DEBUG)
|
|
303
|
-
log("ack_retry", { op_id, code: msg.error_code });
|
|
304
|
-
return send({ type: "AckOk", op_id });
|
|
305
|
-
}
|
|
306
|
-
else if (status === "failed" || status === "dead") {
|
|
307
|
-
ackDead(db, op_id, { code: msg.error_code, message: msg.error_message });
|
|
308
|
-
if (DEBUG)
|
|
309
|
-
log("ack_dead", { op_id, code: msg.error_code });
|
|
310
|
-
return send({ type: "AckOk", op_id });
|
|
311
|
-
}
|
|
312
|
-
return send({ type: "Error", message: `unknown status ${status}` });
|
|
313
|
-
}
|
|
314
|
-
case "QueryStats": {
|
|
315
|
-
const st = queueStats(db);
|
|
316
|
-
if (DEBUG)
|
|
317
|
-
log("stats", st);
|
|
318
|
-
return send({ type: "Stats", ...st });
|
|
319
|
-
}
|
|
320
|
-
case "QueryClients": {
|
|
321
|
-
const status = getWsStatus();
|
|
322
|
-
return send({ type: "Clients", clients: status.clients });
|
|
323
|
-
}
|
|
324
|
-
case "WhoAmI": {
|
|
325
|
-
const meta = clientMeta.get(sock);
|
|
326
|
-
return send({ type: "YouAre", consumerId: meta?.consumerId, lastSeenAt: meta?.lastSeenAt });
|
|
327
|
-
}
|
|
328
|
-
default: {
|
|
329
|
-
return send({ type: "Error", message: "unknown message type" });
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
function safeParseJson(s) {
|
|
334
|
-
try {
|
|
335
|
-
return JSON.parse(s);
|
|
336
|
-
}
|
|
337
|
-
catch {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const DEBUG = envDebug();
|
|
342
|
-
const LOG_FILE = envLogFilePath();
|
|
343
|
-
function envDebug() {
|
|
344
|
-
const v = (process.env.REMNOTE_WS_DEBUG || process.env.WS_DEBUG || '').toLowerCase();
|
|
345
|
-
return v === '1' || v === 'true';
|
|
346
|
-
}
|
|
347
|
-
function log(msg, ctx) {
|
|
348
|
-
try {
|
|
349
|
-
const line = `[ws] ${msg}`;
|
|
350
|
-
const ctxStr = ctx ? JSON.stringify(ctx) : '';
|
|
351
|
-
console.log(line, ctxStr);
|
|
352
|
-
if (LOG_FILE) {
|
|
353
|
-
try {
|
|
354
|
-
const record = `${new Date().toISOString()} ${line} ${ctxStr}\n`;
|
|
355
|
-
fs.appendFileSync(LOG_FILE, record);
|
|
356
|
-
}
|
|
357
|
-
catch (_) { }
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
catch {
|
|
361
|
-
console.log(`[ws] ${msg}`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
function envLogFilePath() {
|
|
365
|
-
const p = process.env.REMNOTE_WS_LOGFILE || process.env.WS_LOGFILE || '';
|
|
366
|
-
const home = process.env.HOME || process.env.USERPROFILE || '.';
|
|
367
|
-
const def = DEBUG ? path.join(home, '.remnote-mcp', 'ws-debug.log') : '';
|
|
368
|
-
const out = p || def;
|
|
369
|
-
if (!out)
|
|
370
|
-
return undefined;
|
|
371
|
-
try {
|
|
372
|
-
const dir = path.dirname(out);
|
|
373
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
374
|
-
}
|
|
375
|
-
catch (_) { }
|
|
376
|
-
return out;
|
|
377
|
-
}
|