@coclaw/openclaw-coclaw 0.1.1 → 0.1.3
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 +1 -1
- package/index.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/common/gateway-notify.js +2 -1
- package/src/homedir-mock.helper.js +47 -0
- package/src/realtime-bridge.js +15 -16
- package/src/session-manager/manager.js +20 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @coclaw/openclaw-coclaw
|
|
2
2
|
|
|
3
|
-
CoClaw 的 OpenClaw 插件(npm: `@coclaw/openclaw-coclaw`,plugin id: `coclaw`),包含:
|
|
3
|
+
CoClaw 的 OpenClaw 插件(npm: `@coclaw/openclaw-coclaw`,plugin id: `openclaw-coclaw`),包含:
|
|
4
4
|
|
|
5
5
|
- **transport bridge** — CoClaw server 与 OpenClaw gateway 之间的实时消息桥接
|
|
6
6
|
- **session-manager** — 会话列表/读取能力(`nativeui.sessions.listAll` / `nativeui.sessions.get`)
|
package/index.js
CHANGED
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -29,6 +29,7 @@ export function callGatewayMethod(method, spawnFn, opts) {
|
|
|
29
29
|
try {
|
|
30
30
|
child = doSpawn('openclaw', ['gateway', 'call', method, '--json'], {
|
|
31
31
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
32
|
+
shell: true, // Windows 上 npm 全局安装生成 .cmd,需经 shell 解析
|
|
32
33
|
});
|
|
33
34
|
} catch {
|
|
34
35
|
resolve({ ok: false, error: 'spawn_failed' });
|
|
@@ -45,7 +46,7 @@ export function callGatewayMethod(method, spawnFn, opts) {
|
|
|
45
46
|
settled = true;
|
|
46
47
|
clearTimeout(timeoutTimer);
|
|
47
48
|
clearTimeout(graceTimer);
|
|
48
|
-
try { child.kill(); } catch {}
|
|
49
|
+
try { child.kill(); } catch {}
|
|
49
50
|
resolve(result);
|
|
50
51
|
};
|
|
51
52
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 跨平台 mock os.homedir()
|
|
3
|
+
*
|
|
4
|
+
* Node.js os.homedir() 在不同平台读取不同环境变量:
|
|
5
|
+
* - POSIX: HOME
|
|
6
|
+
* - Windows: USERPROFILE(优先)、HOMEDRIVE+HOMEPATH
|
|
7
|
+
*
|
|
8
|
+
* 测试中需同时设置两端变量,确保 os.homedir() 返回期望路径。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const HOME_VARS = ['HOME', 'USERPROFILE'];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 保存当前 home 相关环境变量
|
|
15
|
+
* @returns {Record<string, string | undefined>}
|
|
16
|
+
*/
|
|
17
|
+
export function saveHomedir() {
|
|
18
|
+
const saved = {};
|
|
19
|
+
for (const key of HOME_VARS) {
|
|
20
|
+
saved[key] = process.env[key];
|
|
21
|
+
}
|
|
22
|
+
return saved;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 将 home 相关环境变量统一设置为指定路径
|
|
27
|
+
* @param {string} dir - 目标路径
|
|
28
|
+
*/
|
|
29
|
+
export function setHomedir(dir) {
|
|
30
|
+
for (const key of HOME_VARS) {
|
|
31
|
+
process.env[key] = dir;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 恢复之前保存的 home 相关环境变量
|
|
37
|
+
* @param {Record<string, string | undefined>} saved
|
|
38
|
+
*/
|
|
39
|
+
export function restoreHomedir(saved) {
|
|
40
|
+
for (const key of HOME_VARS) {
|
|
41
|
+
if (saved[key] === undefined) {
|
|
42
|
+
delete process.env[key];
|
|
43
|
+
} else {
|
|
44
|
+
process.env[key] = saved[key];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/realtime-bridge.js
CHANGED
|
@@ -152,7 +152,6 @@ async function gatewayRpc(method, params = {}, options = {}) {
|
|
|
152
152
|
});
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
// eslint-disable-next-line no-unused-vars -- 功能暂时禁用,保留函数体供后续修复参考
|
|
156
155
|
async function ensureMainSessionKey() {
|
|
157
156
|
if (mainSessionEnsured) {
|
|
158
157
|
return { ok: true, state: 'ready' };
|
|
@@ -162,25 +161,25 @@ async function ensureMainSessionKey() {
|
|
|
162
161
|
}
|
|
163
162
|
mainSessionEnsurePromise = (async () => {
|
|
164
163
|
const key = 'agent:main:main';
|
|
164
|
+
// sessions.resolve 仅返回 { ok, key },不含 entry
|
|
165
165
|
const resolved = await gatewayRpc('sessions.resolve', { key }, { timeoutMs: 2000 });
|
|
166
|
-
|
|
167
|
-
if (resolved?.ok === true && typeof resolvedSessionId === 'string' && resolvedSessionId.trim()) {
|
|
166
|
+
if (resolved?.ok === true) {
|
|
168
167
|
mainSessionEnsured = true;
|
|
169
|
-
logBridgeDebug(`main session key ensure: ready key=${key}
|
|
170
|
-
return { ok: true, state: 'ready'
|
|
168
|
+
logBridgeDebug(`main session key ensure: ready key=${key}`);
|
|
169
|
+
return { ok: true, state: 'ready' };
|
|
171
170
|
}
|
|
171
|
+
// 仅当网关真实响应 "不存在" 时才创建;超时/网关未就绪等瞬态错误不触发 reset
|
|
172
|
+
if (!resolved?.response) {
|
|
173
|
+
return { ok: false, error: resolved?.error ?? 'resolve_transient_failure' };
|
|
174
|
+
}
|
|
175
|
+
// session key 不存在,通过 sessions.reset 创建
|
|
172
176
|
const reset = await gatewayRpc('sessions.reset', { key, reason: 'new' }, { timeoutMs: 2500 });
|
|
173
177
|
if (reset?.ok !== true) {
|
|
174
178
|
return { ok: false, error: reset?.error ?? 'sessions_reset_failed' };
|
|
175
179
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
mainSessionEnsured = true;
|
|
180
|
-
logBridgeDebug(`main session key ensure: created key=${key} sessionId=${verifySessionId}`);
|
|
181
|
-
return { ok: true, state: 'created', sessionId: verifySessionId };
|
|
182
|
-
}
|
|
183
|
-
return { ok: false, error: verify?.error ?? 'sessions_resolve_after_reset_failed' };
|
|
180
|
+
mainSessionEnsured = true;
|
|
181
|
+
logBridgeDebug(`main session key ensure: created key=${key}`);
|
|
182
|
+
return { ok: true, state: 'created' };
|
|
184
183
|
})();
|
|
185
184
|
try {
|
|
186
185
|
const result = await mainSessionEnsurePromise;
|
|
@@ -259,9 +258,7 @@ function ensureGatewayConnection() {
|
|
|
259
258
|
gatewayReady = true;
|
|
260
259
|
logBridgeDebug(`gateway connect ok <- id=${payload.id}`);
|
|
261
260
|
gatewayConnectReqId = null;
|
|
262
|
-
|
|
263
|
-
// 导致对话被频繁重置。详见 docs/ensure-main-session-bug-analysis.md
|
|
264
|
-
// void ensureMainSessionKey();
|
|
261
|
+
void ensureMainSessionKey();
|
|
265
262
|
}
|
|
266
263
|
else {
|
|
267
264
|
gatewayReady = false;
|
|
@@ -528,6 +525,8 @@ export async function refreshRealtimeBridge() {
|
|
|
528
525
|
|
|
529
526
|
export async function stopRealtimeBridge() {
|
|
530
527
|
started = false;
|
|
528
|
+
mainSessionEnsured = false;
|
|
529
|
+
mainSessionEnsurePromise = null;
|
|
531
530
|
clearConnectTimer();
|
|
532
531
|
if (reconnectTimer) {
|
|
533
532
|
clearTimeout(reconnectTimer);
|
|
@@ -100,7 +100,7 @@ function extractRawTextFromContent(content) {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function findFirstUserRawText(filePath, logger) {
|
|
103
|
-
const lines = fs.readFileSync(filePath, 'utf8').split(
|
|
103
|
+
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
|
|
104
104
|
for (const line of lines) {
|
|
105
105
|
if (!line) continue;
|
|
106
106
|
try {
|
|
@@ -184,10 +184,28 @@ export function createSessionManager(options = {}) {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// 补充 sessions.json 中有索引但无 transcript 文件的 session(如 reset 后未对话、新建 session)
|
|
188
|
+
for (const [sessionKey, entry] of Object.entries(index)) {
|
|
189
|
+
const sid = entry?.sessionId;
|
|
190
|
+
if (!sid || grouped.has(sid)) continue;
|
|
191
|
+
grouped.set(sid, {
|
|
192
|
+
sessionId: sid,
|
|
193
|
+
sessionKey,
|
|
194
|
+
indexed: true,
|
|
195
|
+
archiveType: 'live',
|
|
196
|
+
fileName: null,
|
|
197
|
+
updatedAt: entry.updatedAt ?? 0,
|
|
198
|
+
size: 0,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
187
202
|
const rows = Array.from(grouped.values());
|
|
188
203
|
rows.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
189
204
|
|
|
190
205
|
const items = rows.slice(cursor, cursor + limit).map((row) => {
|
|
206
|
+
if (!row.fileName) {
|
|
207
|
+
return { ...row };
|
|
208
|
+
}
|
|
191
209
|
const transcriptPath = nodePath.join(dir, row.fileName);
|
|
192
210
|
const derivedTitle = deriveTitle(transcriptPath, logger);
|
|
193
211
|
if (!derivedTitle) {
|
|
@@ -249,7 +267,7 @@ export function createSessionManager(options = {}) {
|
|
|
249
267
|
if (!file) throw new Error(`session transcript not found: ${sessionId}`);
|
|
250
268
|
|
|
251
269
|
const all = [];
|
|
252
|
-
for (const line of fs.readFileSync(file, 'utf8').split(
|
|
270
|
+
for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/).filter(Boolean)) {
|
|
253
271
|
try {
|
|
254
272
|
all.push(JSON.parse(line));
|
|
255
273
|
}
|