remnote-bridge 0.1.11 → 0.1.13
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/dist/cli/addon/addon-manager.js +163 -0
- package/dist/cli/addon/registry.js +24 -0
- package/dist/cli/commands/addon.js +149 -0
- package/dist/cli/commands/clean.js +121 -52
- package/dist/cli/commands/connect.js +72 -33
- package/dist/cli/commands/disconnect.js +19 -19
- package/dist/cli/commands/edit-rem.js +8 -36
- package/dist/cli/commands/edit-tree.js +3 -20
- package/dist/cli/commands/health.js +19 -18
- package/dist/cli/commands/read-context.js +3 -20
- package/dist/cli/commands/read-globe.js +3 -20
- package/dist/cli/commands/read-rem.js +6 -32
- package/dist/cli/commands/read-tree.js +3 -20
- package/dist/cli/commands/search.js +97 -21
- package/dist/cli/config.js +148 -72
- package/dist/cli/daemon/daemon.js +104 -24
- package/dist/cli/daemon/dev-server.js +9 -1
- package/dist/cli/daemon/pid.js +36 -22
- package/dist/cli/daemon/registry.js +160 -0
- package/dist/cli/daemon/send-request.js +11 -11
- package/dist/cli/daemon/static-server.js +97 -34
- package/dist/cli/handlers/edit-handler.js +49 -140
- package/dist/cli/handlers/read-handler.js +9 -9
- package/dist/cli/handlers/rem-cache.js +10 -5
- package/dist/cli/handlers/tree-parser.js +16 -9
- package/dist/cli/main.js +67 -19
- package/dist/cli/protocol.js +18 -4
- package/dist/cli/server/config-server.js +280 -14
- package/dist/cli/server/ws-server.js +93 -44
- package/dist/cli/utils/output.js +29 -0
- package/dist/mcp/format.js +43 -0
- package/dist/mcp/index.js +0 -55
- package/dist/mcp/instructions.js +424 -216
- package/dist/mcp/resources/edit-rem-guide.js +37 -158
- package/dist/mcp/resources/edit-tree-guide.js +1 -1
- package/dist/mcp/resources/error-reference.js +9 -13
- package/dist/mcp/resources/rem-object-fields.js +6 -6
- package/dist/mcp/tools/edit-tools.js +69 -8
- package/dist/mcp/tools/infra-tools.js +44 -8
- package/dist/mcp/tools/read-tools.js +136 -20
- package/package.json +2 -2
- package/remnote-plugin/dist/bridge_widget-sandbox.js +17 -17
- package/remnote-plugin/dist/bridge_widget.js +17 -17
- package/remnote-plugin/dist/index-sandbox.js +31 -31
- package/remnote-plugin/dist/index.js +31 -31
- package/remnote-plugin/dist/manifest.json +1 -1
- package/remnote-plugin/package.json +1 -1
- package/remnote-plugin/public/manifest.json +1 -1
- package/remnote-plugin/src/bridge/multi-connection-manager.ts +151 -0
- package/remnote-plugin/src/bridge/websocket-client.ts +62 -16
- package/remnote-plugin/src/services/index.ts +0 -8
- package/remnote-plugin/src/services/read-rem.ts +1 -9
- package/remnote-plugin/src/services/search.ts +13 -10
- package/remnote-plugin/src/settings.ts +9 -7
- package/remnote-plugin/src/utils/index.ts +0 -5
- package/remnote-plugin/src/widgets/bridge_widget.tsx +105 -20
- package/remnote-plugin/src/widgets/index.tsx +41 -44
- package/remnote-plugin/webpack.config.js +35 -0
- package/skills/remnote-bridge/SKILL.md +45 -40
- package/skills/remnote-bridge/instructions/addon.md +134 -0
- package/skills/remnote-bridge/instructions/clean.md +110 -0
- package/skills/remnote-bridge/instructions/connect.md +80 -37
- package/skills/remnote-bridge/instructions/disconnect.md +22 -9
- package/skills/remnote-bridge/instructions/edit-rem.md +113 -327
- package/skills/remnote-bridge/instructions/health.md +23 -13
- package/skills/remnote-bridge/instructions/install-skill.md +58 -0
- package/skills/remnote-bridge/instructions/overall.md +99 -35
- package/skills/remnote-bridge/instructions/read-rem.md +15 -15
- package/skills/remnote-bridge/instructions/search.md +77 -18
- package/skills/remnote-bridge/instructions/setup.md +5 -6
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EditHandler — edit-rem 命令的编排器
|
|
3
3
|
*
|
|
4
|
-
* 在 daemon
|
|
4
|
+
* 在 daemon 层实现两道防线 + 字段白名单校验 + 直接转发 changes。
|
|
5
5
|
* Plugin 只负责原子写入(write_rem_fields)。
|
|
6
6
|
*
|
|
7
7
|
* 防线 1:缓存存在性检查(必须先 read 再 edit)
|
|
8
8
|
* 防线 2:乐观并发检测(当前 JSON 与缓存 JSON 比较)
|
|
9
|
-
* 防线 3:str_replace 精确匹配(old_str 必须唯一匹配)
|
|
10
|
-
*
|
|
11
|
-
* Portal 专用路径:type === 'portal' 时,在简化 JSON(9 字段)上执行 str_replace,
|
|
12
|
-
* 推导变更后调用专用写入逻辑。
|
|
13
9
|
*/
|
|
14
|
-
import { PORTAL_FIELDS } from './read-handler.js';
|
|
15
10
|
/** 只读字段集合 — 变更这些字段只产生警告,不执行写入 */
|
|
16
11
|
const READ_ONLY_FIELDS = new Set([
|
|
17
12
|
'id',
|
|
@@ -30,10 +25,22 @@ const READ_ONLY_FIELDS = new Set([
|
|
|
30
25
|
'isPowerup', 'isPowerupEnum', 'isPowerupProperty',
|
|
31
26
|
'isPowerupPropertyListItem', 'isPowerupSlot',
|
|
32
27
|
]);
|
|
33
|
-
/**
|
|
34
|
-
const
|
|
35
|
-
'
|
|
28
|
+
/** 可写字段白名单 — 21 个可写字段 */
|
|
29
|
+
const WRITABLE_FIELDS = new Set([
|
|
30
|
+
'text', 'backText', 'type', 'isDocument', 'parent',
|
|
31
|
+
'fontSize', 'highlightColor',
|
|
32
|
+
'isTodo', 'todoStatus', 'isCode', 'isQuote', 'isListItem', 'isCardItem', 'isSlot', 'isProperty',
|
|
33
|
+
'enablePractice', 'practiceDirection',
|
|
34
|
+
'tags', 'sources', 'positionAmongstSiblings', 'portalDirectlyIncludedRem',
|
|
36
35
|
]);
|
|
36
|
+
/** 枚举字段的合法值 */
|
|
37
|
+
const ENUM_VALUES = {
|
|
38
|
+
type: new Set(['concept', 'descriptor', 'default']),
|
|
39
|
+
practiceDirection: new Set(['forward', 'backward', 'both', 'none']),
|
|
40
|
+
highlightColor: new Set(['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Purple', 'Gray', 'Brown', 'Pink', null]),
|
|
41
|
+
fontSize: new Set(['H1', 'H2', 'H3', null]),
|
|
42
|
+
todoStatus: new Set(['Finished', 'Unfinished', null]),
|
|
43
|
+
};
|
|
37
44
|
export class EditHandler {
|
|
38
45
|
cache;
|
|
39
46
|
forwardToPlugin;
|
|
@@ -42,145 +49,61 @@ export class EditHandler {
|
|
|
42
49
|
this.forwardToPlugin = forwardToPlugin;
|
|
43
50
|
}
|
|
44
51
|
async handleEditRem(payload) {
|
|
45
|
-
const { remId,
|
|
52
|
+
const { remId, changes } = payload;
|
|
53
|
+
if (!changes || typeof changes !== 'object' || Object.keys(changes).length === 0) {
|
|
54
|
+
return { ok: true, changes: [], warnings: [] };
|
|
55
|
+
}
|
|
46
56
|
// ── 防线 1: 缓存存在性检查 ──
|
|
47
|
-
const
|
|
48
|
-
if (!
|
|
57
|
+
const cachedObj = this.cache.get('rem:' + remId);
|
|
58
|
+
if (!cachedObj) {
|
|
49
59
|
throw new Error(`Rem ${remId} has not been read yet. Read it first before editing.`);
|
|
50
60
|
}
|
|
51
61
|
// ── 防线 2: 乐观并发检测 ──
|
|
52
62
|
const currentRemObject = await this.forwardToPlugin('read_rem', { remId });
|
|
53
63
|
const currentJson = JSON.stringify(currentRemObject, null, 2);
|
|
64
|
+
const cachedJson = JSON.stringify(cachedObj, null, 2);
|
|
54
65
|
if (currentJson !== cachedJson) {
|
|
55
66
|
// 不更新缓存 — 迫使 AI re-read
|
|
56
67
|
throw new Error(`Rem ${remId} has been modified since last read. Please read it again before editing.`);
|
|
57
68
|
}
|
|
58
|
-
// ──
|
|
59
|
-
const cachedObj = JSON.parse(cachedJson);
|
|
60
|
-
if (cachedObj.type === 'portal') {
|
|
61
|
-
return this.handlePortalEdit(remId, cachedJson, cachedObj, oldStr, newStr);
|
|
62
|
-
}
|
|
63
|
-
// ── 普通 Rem 路径 ──
|
|
64
|
-
return this.handleNormalEdit(remId, cachedJson, cachedObj, oldStr, newStr);
|
|
65
|
-
}
|
|
66
|
-
/** Portal 专用编辑路径:在简化 JSON 上执行 str_replace */
|
|
67
|
-
async handlePortalEdit(remId, cachedJson, fullObj, oldStr, newStr) {
|
|
68
|
-
// 从完整对象提取简化 JSON
|
|
69
|
-
const simplified = {};
|
|
70
|
-
for (const field of PORTAL_FIELDS) {
|
|
71
|
-
if (field in fullObj) {
|
|
72
|
-
simplified[field] = fullObj[field];
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const simplifiedJson = JSON.stringify(simplified, null, 2);
|
|
76
|
-
// ── 防线 3: str_replace 在简化 JSON 上精确匹配 ──
|
|
77
|
-
const matchCount = countOccurrences(simplifiedJson, oldStr);
|
|
78
|
-
if (matchCount === 0) {
|
|
79
|
-
throw new Error(`old_str not found in the simplified Portal JSON of rem ${remId}`);
|
|
80
|
-
}
|
|
81
|
-
if (matchCount > 1) {
|
|
82
|
-
throw new Error(`old_str matches ${matchCount} locations in Portal rem ${remId}. ` +
|
|
83
|
-
`Make old_str more specific to match exactly once.`);
|
|
84
|
-
}
|
|
85
|
-
const modifiedSimplifiedJson = simplifiedJson.replace(oldStr, newStr);
|
|
86
|
-
// JSON 解析
|
|
87
|
-
let modified;
|
|
88
|
-
try {
|
|
89
|
-
modified = JSON.parse(modifiedSimplifiedJson);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
throw new Error('The replacement produced invalid JSON. Check your new_str for syntax errors.');
|
|
93
|
-
}
|
|
94
|
-
// 推导变更字段
|
|
95
|
-
const changes = {};
|
|
69
|
+
// ── 遍历 changes keys:分类过滤 ──
|
|
96
70
|
const warnings = [];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
71
|
+
const writableChanges = {};
|
|
72
|
+
for (const key of Object.keys(changes)) {
|
|
73
|
+
if (READ_ONLY_FIELDS.has(key)) {
|
|
74
|
+
warnings.push(`Field '${key}' is read-only and was ignored`);
|
|
75
|
+
}
|
|
76
|
+
else if (!WRITABLE_FIELDS.has(key)) {
|
|
77
|
+
warnings.push(`Field '${key}' is unknown and was ignored`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
writableChanges[key] = changes[key];
|
|
105
81
|
}
|
|
106
82
|
}
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
remId,
|
|
114
|
-
changes,
|
|
115
|
-
}));
|
|
116
|
-
if (writeResult.failed) {
|
|
117
|
-
return {
|
|
118
|
-
ok: false,
|
|
119
|
-
changes: [],
|
|
120
|
-
warnings,
|
|
121
|
-
error: `Failed to update field '${writeResult.failed.field}': ${writeResult.failed.error}`,
|
|
122
|
-
appliedChanges: writeResult.applied,
|
|
123
|
-
failedField: writeResult.failed.field,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
// ── 写入成功 → 从 Plugin 重新获取完整 Rem 并更新缓存(D5)──
|
|
127
|
-
const freshRemObject = await this.forwardToPlugin('read_rem', { remId });
|
|
128
|
-
const freshJson = JSON.stringify(freshRemObject, null, 2);
|
|
129
|
-
this.cache.set('rem:' + remId, freshJson);
|
|
130
|
-
return {
|
|
131
|
-
ok: true,
|
|
132
|
-
changes: Object.keys(changes),
|
|
133
|
-
warnings,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
/** 普通 Rem 编辑路径:在完整 JSON 上执行 str_replace */
|
|
137
|
-
async handleNormalEdit(remId, cachedJson, original, oldStr, newStr) {
|
|
138
|
-
// ── 防线 3: str_replace 精确匹配 ──
|
|
139
|
-
const matchCount = countOccurrences(cachedJson, oldStr);
|
|
140
|
-
if (matchCount === 0) {
|
|
141
|
-
throw new Error(`old_str not found in the serialized JSON of rem ${remId}`);
|
|
142
|
-
}
|
|
143
|
-
if (matchCount > 1) {
|
|
144
|
-
throw new Error(`old_str matches ${matchCount} locations in rem ${remId}. ` +
|
|
145
|
-
`Make old_str more specific to match exactly once.`);
|
|
146
|
-
}
|
|
147
|
-
const modifiedJson = cachedJson.replace(oldStr, newStr);
|
|
148
|
-
// 1. JSON 解析
|
|
149
|
-
let modified;
|
|
150
|
-
try {
|
|
151
|
-
modified = JSON.parse(modifiedJson);
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
throw new Error('The replacement produced invalid JSON. Check your new_str for syntax errors.');
|
|
155
|
-
}
|
|
156
|
-
// 2. 推导变更字段
|
|
157
|
-
const changes = {};
|
|
158
|
-
const warnings = [];
|
|
159
|
-
for (const key of Object.keys(modified)) {
|
|
160
|
-
if (JSON.stringify(modified[key]) !== JSON.stringify(original[key])) {
|
|
161
|
-
if (READ_ONLY_FIELDS.has(key)) {
|
|
162
|
-
warnings.push(`Field '${key}' is read-only and was ignored`);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
changes[key] = modified[key];
|
|
83
|
+
// ── 枚举值范围校验 ──
|
|
84
|
+
for (const [field, allowedValues] of Object.entries(ENUM_VALUES)) {
|
|
85
|
+
if (field in writableChanges) {
|
|
86
|
+
const value = writableChanges[field];
|
|
87
|
+
if (!allowedValues.has(value)) {
|
|
88
|
+
throw new Error(`Invalid value for '${field}': ${JSON.stringify(value)}. Allowed: ${[...allowedValues].map(v => JSON.stringify(v)).join(', ')}`);
|
|
166
89
|
}
|
|
167
90
|
}
|
|
168
91
|
}
|
|
169
|
-
//
|
|
170
|
-
if ('todoStatus' in
|
|
171
|
-
const isTodo =
|
|
92
|
+
// ── 语义校验:todoStatus 非 null 但 isTodo 未启用 ──
|
|
93
|
+
if ('todoStatus' in writableChanges && writableChanges.todoStatus !== null) {
|
|
94
|
+
const isTodo = writableChanges.isTodo ?? cachedObj.isTodo;
|
|
172
95
|
if (!isTodo) {
|
|
173
96
|
warnings.push("Setting 'todoStatus' without 'isTodo: true' may have no effect");
|
|
174
97
|
}
|
|
175
98
|
}
|
|
176
|
-
//
|
|
177
|
-
if (Object.keys(
|
|
99
|
+
// ── 空变更检查 ──
|
|
100
|
+
if (Object.keys(writableChanges).length === 0) {
|
|
178
101
|
return { ok: true, changes: [], warnings };
|
|
179
102
|
}
|
|
180
103
|
// ── 发送变更到 Plugin ──
|
|
181
104
|
const writeResult = (await this.forwardToPlugin('write_rem_fields', {
|
|
182
105
|
remId,
|
|
183
|
-
changes,
|
|
106
|
+
changes: writableChanges,
|
|
184
107
|
}));
|
|
185
108
|
if (writeResult.failed) {
|
|
186
109
|
// 部分失败 — 不更新缓存(迫使 AI re-read)
|
|
@@ -193,27 +116,13 @@ export class EditHandler {
|
|
|
193
116
|
failedField: writeResult.failed.field,
|
|
194
117
|
};
|
|
195
118
|
}
|
|
196
|
-
// ── 写入成功 → 从 Plugin 重新获取完整 Rem
|
|
119
|
+
// ── 写入成功 → 从 Plugin 重新获取完整 Rem 并更新缓存 ──
|
|
197
120
|
const freshRemObject = await this.forwardToPlugin('read_rem', { remId });
|
|
198
|
-
|
|
199
|
-
this.cache.set('rem:' + remId, freshJson);
|
|
121
|
+
this.cache.set('rem:' + remId, freshRemObject);
|
|
200
122
|
return {
|
|
201
123
|
ok: true,
|
|
202
|
-
changes: Object.keys(
|
|
124
|
+
changes: Object.keys(writableChanges),
|
|
203
125
|
warnings,
|
|
204
126
|
};
|
|
205
127
|
}
|
|
206
128
|
}
|
|
207
|
-
/** 统计 needle 在 haystack 中出现的次数 */
|
|
208
|
-
function countOccurrences(haystack, needle) {
|
|
209
|
-
let count = 0;
|
|
210
|
-
let pos = 0;
|
|
211
|
-
while (true) {
|
|
212
|
-
pos = haystack.indexOf(needle, pos);
|
|
213
|
-
if (pos === -1)
|
|
214
|
-
break;
|
|
215
|
-
count++;
|
|
216
|
-
pos += needle.length; // 非重叠匹配,与 String.replace 行为一致
|
|
217
|
-
}
|
|
218
|
-
return count;
|
|
219
|
-
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/** R-F 字段(仅 --full 模式输出,默认不输出) */
|
|
10
10
|
const RF_FIELDS = new Set([
|
|
11
|
+
'children',
|
|
11
12
|
'isPowerup', 'isPowerupEnum', 'isPowerupProperty',
|
|
12
13
|
'isPowerupPropertyListItem', 'isPowerupSlot',
|
|
13
14
|
'deepRemsBeingReferenced',
|
|
@@ -17,10 +18,10 @@ const RF_FIELDS = new Set([
|
|
|
17
18
|
'embeddedQueueViewMode',
|
|
18
19
|
'localUpdatedAt', 'lastPracticed',
|
|
19
20
|
]);
|
|
20
|
-
/** Portal 简化输出字段(type === 'portal' 时默认输出这
|
|
21
|
+
/** Portal 简化输出字段(type === 'portal' 时默认输出这 8 个字段) */
|
|
21
22
|
export const PORTAL_FIELDS = [
|
|
22
23
|
'id', 'type', 'portalType', 'portalDirectlyIncludedRem',
|
|
23
|
-
'parent', 'positionAmongstSiblings',
|
|
24
|
+
'parent', 'positionAmongstSiblings',
|
|
24
25
|
'createdAt', 'updatedAt',
|
|
25
26
|
];
|
|
26
27
|
export class ReadHandler {
|
|
@@ -43,17 +44,16 @@ export class ReadHandler {
|
|
|
43
44
|
const includePowerup = payload.includePowerup ?? false;
|
|
44
45
|
// 转发到 Plugin
|
|
45
46
|
const remObject = await this.forwardToPlugin('read_rem', { remId, includePowerup });
|
|
46
|
-
// 缓存完整
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
this.onLog?.(`缓存 Rem ${remId.slice(0, 8)}... (${fullJson.length} bytes)`, 'info');
|
|
47
|
+
// 缓存完整 RemObject 对象
|
|
48
|
+
this.cache.set(cacheKey, remObject);
|
|
49
|
+
this.onLog?.(`缓存 Rem ${remId.slice(0, 8)}...`, 'info');
|
|
50
50
|
// 字段过滤
|
|
51
51
|
const fields = payload.fields;
|
|
52
52
|
const full = payload.full;
|
|
53
53
|
let result;
|
|
54
54
|
if (full) {
|
|
55
|
-
// --full → 返回完整对象(含 R-F
|
|
56
|
-
result = remObject;
|
|
55
|
+
// --full → 返回完整对象(含 R-F 字段)。浅拷贝避免污染缓存对象。
|
|
56
|
+
result = { ...remObject };
|
|
57
57
|
}
|
|
58
58
|
else if (fields) {
|
|
59
59
|
// --fields 过滤:只返回指定字段 + id
|
|
@@ -66,7 +66,7 @@ export class ReadHandler {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
else if (remObject.type === 'portal') {
|
|
69
|
-
// Portal 简化模式:只输出
|
|
69
|
+
// Portal 简化模式:只输出 8 个关键字段
|
|
70
70
|
const obj = remObject;
|
|
71
71
|
result = {};
|
|
72
72
|
for (const field of PORTAL_FIELDS) {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RemCache — LRU 缓存,存储 Rem
|
|
2
|
+
* RemCache — LRU 缓存,存储 Rem 数据
|
|
3
3
|
*
|
|
4
4
|
* 缓存存储在 daemon 内存中,生命周期与 daemon 一致。
|
|
5
5
|
* disconnect 关闭 daemon → 缓存自然消失。
|
|
6
|
+
*
|
|
7
|
+
* 泛化值类型:不同 key 前缀存储不同类型的数据:
|
|
8
|
+
* - rem:{remId} → RemObject 对象
|
|
9
|
+
* - tree:{remId} → Markdown outline 字符串
|
|
10
|
+
* - tree-depth:{remId} 等 → 参数值字符串
|
|
6
11
|
*/
|
|
7
12
|
export class RemCache {
|
|
8
13
|
cache = new Map();
|
|
@@ -15,19 +20,19 @@ export class RemCache {
|
|
|
15
20
|
if (!entry)
|
|
16
21
|
return null;
|
|
17
22
|
entry.lastAccess = Date.now();
|
|
18
|
-
return entry.
|
|
23
|
+
return entry.data;
|
|
19
24
|
}
|
|
20
25
|
/** 获取缓存条目的创建时间(ISO 8601),不存在返回 null */
|
|
21
26
|
getCreatedAt(remId) {
|
|
22
27
|
const entry = this.cache.get(remId);
|
|
23
28
|
return entry ? entry.createdAt : null;
|
|
24
29
|
}
|
|
25
|
-
set(remId,
|
|
30
|
+
set(remId, data) {
|
|
26
31
|
const now = Date.now();
|
|
27
32
|
const createdAt = new Date(now).toISOString();
|
|
28
33
|
if (this.cache.has(remId)) {
|
|
29
34
|
const entry = this.cache.get(remId);
|
|
30
|
-
entry.
|
|
35
|
+
entry.data = data;
|
|
31
36
|
entry.lastAccess = now;
|
|
32
37
|
entry.createdAt = createdAt;
|
|
33
38
|
return;
|
|
@@ -46,7 +51,7 @@ export class RemCache {
|
|
|
46
51
|
this.cache.delete(oldestId);
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
|
-
this.cache.set(remId, {
|
|
54
|
+
this.cache.set(remId, { data, lastAccess: now, createdAt });
|
|
50
55
|
}
|
|
51
56
|
has(remId) {
|
|
52
57
|
return this.cache.has(remId);
|
|
@@ -197,15 +197,22 @@ export function parsePowerupPrefix(rawContent) {
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
// 尾部箭头(无 backText,multiline)
|
|
200
|
+
// 支持有空格 ` ↓` 和无空格 `)↓` 两种写法(模型常漏空格)
|
|
200
201
|
if (backText === undefined) {
|
|
201
|
-
const
|
|
202
|
-
['
|
|
203
|
-
['
|
|
204
|
-
['
|
|
202
|
+
const tailArrowChars = [
|
|
203
|
+
['↕', 'both'],
|
|
204
|
+
['↓', 'forward'],
|
|
205
|
+
['↑', 'backward'],
|
|
205
206
|
];
|
|
206
|
-
for (const [
|
|
207
|
-
if (content.endsWith(
|
|
208
|
-
content = content.slice(0, -
|
|
207
|
+
for (const [ch, dir] of tailArrowChars) {
|
|
208
|
+
if (content.endsWith(` ${ch}`)) {
|
|
209
|
+
content = content.slice(0, -(ch.length + 1)); // 去掉 ` ↓`
|
|
210
|
+
practiceDirection = dir;
|
|
211
|
+
isMultiline = true;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
if (content.endsWith(ch)) {
|
|
215
|
+
content = content.slice(0, -ch.length); // 去掉 `↓`(无空格)
|
|
209
216
|
practiceDirection = dir;
|
|
210
217
|
isMultiline = true;
|
|
211
218
|
break;
|
|
@@ -227,9 +234,9 @@ export function parsePowerupPrefix(rawContent) {
|
|
|
227
234
|
return result;
|
|
228
235
|
}
|
|
229
236
|
// ────────────────────────── Multiline 检测 ──────────────────────────
|
|
230
|
-
/** multiline 箭头正则:中间箭头 ↓↑↕ 或尾部箭头
|
|
237
|
+
/** multiline 箭头正则:中间箭头 ↓↑↕ 或尾部箭头 ↓↑↕(允许有无空格) */
|
|
231
238
|
const MULTILINE_MID_RE = / [↓↑↕] /;
|
|
232
|
-
const MULTILINE_TAIL_RE = /
|
|
239
|
+
const MULTILINE_TAIL_RE = /[↓↑↕]$/;
|
|
233
240
|
/** 从行内容判断是否为 multiline 父节点(内容包含 ↓↑↕ 箭头) */
|
|
234
241
|
export function isContentMultiline(rawContent) {
|
|
235
242
|
return MULTILINE_MID_RE.test(rawContent) || MULTILINE_TAIL_RE.test(rawContent);
|
package/dist/cli/main.js
CHANGED
|
@@ -19,6 +19,7 @@ import { readContextCommand } from './commands/read-context.js';
|
|
|
19
19
|
import { searchCommand } from './commands/search.js';
|
|
20
20
|
import { installSkillCommand, installSkillCopyCommand } from './commands/install-skill.js';
|
|
21
21
|
import { cleanCommand } from './commands/clean.js';
|
|
22
|
+
import { addonListCommand, addonInstallCommand, addonUninstallCommand } from './commands/addon.js';
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
23
24
|
const { version } = require('../../package.json');
|
|
24
25
|
const program = new Command();
|
|
@@ -59,7 +60,23 @@ program
|
|
|
59
60
|
.name('remnote-bridge')
|
|
60
61
|
.description('RemNote Bridge — CLI + MCP Server + Plugin')
|
|
61
62
|
.version(version)
|
|
62
|
-
.option('--json', '以 JSON 格式输出(适用于程序化调用)')
|
|
63
|
+
.option('--json', '以 JSON 格式输出(适用于程序化调用)')
|
|
64
|
+
.option('--instance <name>', '指定 daemon 实例名(也可用 REMNOTE_BRIDGE_INSTANCE 环境变量)')
|
|
65
|
+
.option('--headless', '使用 headless 实例(覆盖 --instance,也可用 REMNOTE_HEADLESS=1 环境变量)');
|
|
66
|
+
// 全局参数同步到环境变量,使所有命令中的 resolveInstanceId() 自动生效
|
|
67
|
+
program.hook('preAction', () => {
|
|
68
|
+
const opts = program.opts();
|
|
69
|
+
const headlessEnv = process.env.REMNOTE_HEADLESS;
|
|
70
|
+
const isHeadless = opts.headless || headlessEnv === '1' || headlessEnv === 'true';
|
|
71
|
+
if (isHeadless) {
|
|
72
|
+
// headless 覆盖 instance,固定实例名
|
|
73
|
+
process.env.REMNOTE_HEADLESS = '1';
|
|
74
|
+
process.env.REMNOTE_BRIDGE_INSTANCE = 'headless';
|
|
75
|
+
}
|
|
76
|
+
else if (opts.instance) {
|
|
77
|
+
process.env.REMNOTE_BRIDGE_INSTANCE = opts.instance;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
63
80
|
program
|
|
64
81
|
.command('setup')
|
|
65
82
|
.description('启动 Chrome 让用户登录 RemNote(headless 模式前置步骤)')
|
|
@@ -71,11 +88,10 @@ program
|
|
|
71
88
|
.command('connect')
|
|
72
89
|
.description('启动守护进程,等待 Plugin 连接')
|
|
73
90
|
.option('--dev', '开发模式:使用 webpack-dev-server(支持 HMR)')
|
|
74
|
-
.option('--
|
|
75
|
-
.option('--remote-debugging-port <port>', 'Chrome 远程调试端口(仅 --headless)', parseInt)
|
|
91
|
+
.option('--remote-debugging-port <port>', 'Chrome 远程调试端口(仅 headless 模式)', parseInt)
|
|
76
92
|
.action(async (cmdOpts) => {
|
|
77
|
-
const { json } = program.opts();
|
|
78
|
-
await connectCommand({ json, dev: cmdOpts.dev,
|
|
93
|
+
const { json, instance } = program.opts();
|
|
94
|
+
await connectCommand({ json, instance, dev: cmdOpts.dev, remoteDebuggingPort: cmdOpts.remoteDebuggingPort });
|
|
79
95
|
});
|
|
80
96
|
program
|
|
81
97
|
.command('health')
|
|
@@ -83,15 +99,15 @@ program
|
|
|
83
99
|
.option('--diagnose', '诊断 headless Chrome(截图 + 状态 + console 错误)')
|
|
84
100
|
.option('--reload', '重载 headless Chrome 页面')
|
|
85
101
|
.action(async (cmdOpts) => {
|
|
86
|
-
const { json } = program.opts();
|
|
87
|
-
await healthCommand({ json, diagnose: cmdOpts.diagnose, reload: cmdOpts.reload });
|
|
102
|
+
const { json, instance } = program.opts();
|
|
103
|
+
await healthCommand({ json, instance, diagnose: cmdOpts.diagnose, reload: cmdOpts.reload });
|
|
88
104
|
});
|
|
89
105
|
program
|
|
90
106
|
.command('disconnect')
|
|
91
107
|
.description('停止守护进程,释放端口和资源')
|
|
92
108
|
.action(async () => {
|
|
93
|
-
const { json } = program.opts();
|
|
94
|
-
await disconnectCommand({ json });
|
|
109
|
+
const { json, instance } = program.opts();
|
|
110
|
+
await disconnectCommand({ json, instance });
|
|
95
111
|
});
|
|
96
112
|
program
|
|
97
113
|
.command('read-rem [remIdOrJson]')
|
|
@@ -105,7 +121,7 @@ program
|
|
|
105
121
|
const input = parseJsonInput('read-rem', remIdOrJson);
|
|
106
122
|
if (!input)
|
|
107
123
|
return;
|
|
108
|
-
await readRemCommand(input.remId, { json, fields: input.fields
|
|
124
|
+
await readRemCommand(input.remId, { json, fields: input.fields, full: input.full, includePowerup: input.includePowerup });
|
|
109
125
|
}
|
|
110
126
|
else {
|
|
111
127
|
if (!remIdOrJson) {
|
|
@@ -266,7 +282,7 @@ program
|
|
|
266
282
|
process.exitCode = 1;
|
|
267
283
|
return;
|
|
268
284
|
}
|
|
269
|
-
await searchCommand(input.query, { json, limit: input.numResults?.toString() });
|
|
285
|
+
await searchCommand(input.query, { json, limit: (input.limit ?? input.numResults)?.toString() });
|
|
270
286
|
}
|
|
271
287
|
else {
|
|
272
288
|
if (!queryOrJson) {
|
|
@@ -279,16 +295,15 @@ program
|
|
|
279
295
|
});
|
|
280
296
|
program
|
|
281
297
|
.command('edit-rem [remIdOrJson]')
|
|
282
|
-
.description('
|
|
283
|
-
.option('--
|
|
284
|
-
.option('--new-str <newStr>', '替换后的新文本片段')
|
|
298
|
+
.description('直接修改 Rem 的属性字段')
|
|
299
|
+
.option('--changes <changesJson>', '要修改的字段及新值(JSON 字符串)')
|
|
285
300
|
.action(async (remIdOrJson, cmdOpts) => {
|
|
286
301
|
const { json } = program.opts();
|
|
287
302
|
if (json) {
|
|
288
|
-
const input = parseJsonInput('edit-rem', remIdOrJson, ['
|
|
303
|
+
const input = parseJsonInput('edit-rem', remIdOrJson, ['changes']);
|
|
289
304
|
if (!input)
|
|
290
305
|
return;
|
|
291
|
-
await editRemCommand(input.remId, { json,
|
|
306
|
+
await editRemCommand(input.remId, { json, changes: input.changes });
|
|
292
307
|
}
|
|
293
308
|
else {
|
|
294
309
|
if (!remIdOrJson) {
|
|
@@ -296,12 +311,21 @@ program
|
|
|
296
311
|
process.exitCode = 1;
|
|
297
312
|
return;
|
|
298
313
|
}
|
|
299
|
-
if (!cmdOpts.
|
|
300
|
-
console.error('错误: --
|
|
314
|
+
if (!cmdOpts.changes) {
|
|
315
|
+
console.error('错误: --changes 是必需的');
|
|
301
316
|
process.exitCode = 1;
|
|
302
317
|
return;
|
|
303
318
|
}
|
|
304
|
-
|
|
319
|
+
let changes;
|
|
320
|
+
try {
|
|
321
|
+
changes = JSON.parse(cmdOpts.changes);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
console.error('错误: --changes 不是合法的 JSON');
|
|
325
|
+
process.exitCode = 1;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
await editRemCommand(remIdOrJson, { json, changes });
|
|
305
329
|
}
|
|
306
330
|
});
|
|
307
331
|
// mcp 子命令
|
|
@@ -331,4 +355,28 @@ program
|
|
|
331
355
|
const { json } = program.opts();
|
|
332
356
|
await cleanCommand({ json });
|
|
333
357
|
});
|
|
358
|
+
// addon 子命令组
|
|
359
|
+
const addonCmd = program.command('addon').description('管理增强项目(addon)');
|
|
360
|
+
addonCmd
|
|
361
|
+
.command('list')
|
|
362
|
+
.description('查看所有增强项目状态')
|
|
363
|
+
.action(async () => {
|
|
364
|
+
const { json } = program.opts();
|
|
365
|
+
await addonListCommand({ json });
|
|
366
|
+
});
|
|
367
|
+
addonCmd
|
|
368
|
+
.command('install <name>')
|
|
369
|
+
.description('安装指定增强项目')
|
|
370
|
+
.action(async (name) => {
|
|
371
|
+
const { json } = program.opts();
|
|
372
|
+
await addonInstallCommand(name, { json });
|
|
373
|
+
});
|
|
374
|
+
addonCmd
|
|
375
|
+
.command('uninstall <name>')
|
|
376
|
+
.description('卸载指定增强项目')
|
|
377
|
+
.option('--purge', '同时删除数据目录')
|
|
378
|
+
.action(async (name, cmdOpts) => {
|
|
379
|
+
const { json } = program.opts();
|
|
380
|
+
await addonUninstallCommand(name, { json, purge: cmdOpts.purge });
|
|
381
|
+
});
|
|
334
382
|
program.parse();
|
package/dist/cli/protocol.js
CHANGED
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
// ── 消息类型判断辅助 ──
|
|
8
8
|
export function isHelloMessage(msg) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
if (typeof msg !== 'object' || msg === null)
|
|
10
|
+
return false;
|
|
11
|
+
const obj = msg;
|
|
12
|
+
return (obj.type === 'hello' &&
|
|
13
|
+
typeof obj.version === 'string' &&
|
|
14
|
+
typeof obj.twinSlotIndex === 'number' &&
|
|
15
|
+
Number.isInteger(obj.twinSlotIndex) &&
|
|
16
|
+
obj.twinSlotIndex >= 0 &&
|
|
17
|
+
obj.twinSlotIndex <= 3);
|
|
13
18
|
}
|
|
14
19
|
export function isPingMessage(msg) {
|
|
15
20
|
return (typeof msg === 'object' &&
|
|
@@ -33,3 +38,12 @@ export function isBridgeResponse(msg) {
|
|
|
33
38
|
typeof msg.id === 'string' &&
|
|
34
39
|
!('action' in msg));
|
|
35
40
|
}
|
|
41
|
+
// ── WS Close Codes ──
|
|
42
|
+
/** 已有其他 Plugin 连接(非孪生),拒绝 */
|
|
43
|
+
export const WS_CLOSE_OTHER_CONNECTED = 4000;
|
|
44
|
+
/** 心跳超时,断开连接 */
|
|
45
|
+
export const WS_CLOSE_PONG_TIMEOUT = 4001;
|
|
46
|
+
/** 被孪生 Plugin 抢占(daemon 主动断开非孪生连接) */
|
|
47
|
+
export const WS_CLOSE_PREEMPTED = 4002;
|
|
48
|
+
/** 孪生已连,拒绝非孪生 */
|
|
49
|
+
export const WS_CLOSE_TWIN_EXISTS = 4003;
|