@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 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
@@ -47,7 +47,7 @@ function respondError(respond, err) {
47
47
 
48
48
  /* c8 ignore start */
49
49
  const plugin = {
50
- id: 'coclaw',
50
+ id: 'openclaw-coclaw',
51
51
  name: 'CoClaw',
52
52
  description: 'OpenClaw CoClaw channel plugin for remote chat',
53
53
  register(api) {
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "coclaw",
2
+ "id": "openclaw-coclaw",
3
3
  "name": "CoClaw",
4
4
  "description": "OpenClaw CoClaw channel plugin for remote chat",
5
5
  "channels": ["coclaw"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coclaw/openclaw-coclaw",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "OpenClaw CoClaw channel plugin for remote chat",
@@ -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 {} // eslint-disable-line no-empty
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
+ }
@@ -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
- const resolvedSessionId = resolved?.response?.result?.entry?.sessionId;
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} sessionId=${resolvedSessionId}`);
170
- return { ok: true, state: 'ready', sessionId: resolvedSessionId };
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
- const verify = await gatewayRpc('sessions.resolve', { key }, { timeoutMs: 2000 });
177
- const verifySessionId = verify?.response?.result?.entry?.sessionId;
178
- if (verify?.ok === true && typeof verifySessionId === 'string' && verifySessionId.trim()) {
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
- // [DISABLED] ensureMainSessionKey 存在 bug,每次重连都会误触 sessions.reset
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('\n');
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('\n').filter(Boolean)) {
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
  }