lark-remote 0.2.1 → 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 +29 -0
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/dist/binder.d.ts +9 -16
- package/dist/binder.js +14 -40
- package/dist/bridge/index.d.ts +2 -6
- package/dist/bridge/index.js +0 -4
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +47 -3
- package/dist/clone.d.ts +139 -0
- package/dist/clone.js +415 -0
- package/dist/config/codex-config.js +79 -24
- package/dist/config/dir.js +15 -5
- package/dist/config/kimi-config.js +16 -5
- package/dist/connector/index.d.ts +19 -1
- package/dist/connector/index.js +57 -39
- package/dist/index.js +36 -13
- package/dist/platform/command.d.ts +2 -0
- package/dist/platform/command.js +54 -2
- package/dist/restart.d.ts +17 -0
- package/dist/restart.js +38 -19
- package/dist/router/index.d.ts +10 -1
- package/dist/router/index.js +59 -14
- package/package.json +2 -2
package/dist/router/index.js
CHANGED
|
@@ -49,6 +49,25 @@ function pageSlice(items, offset, pageSize) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
const ACTIVE_PAGE_SIZE = 20;
|
|
52
|
+
/**
|
|
53
|
+
* 飞书会把极短间隔连发的多条消息合并成一条换行分隔的消息:用户侧看是两条消息,
|
|
54
|
+
* 本端收到的却是 `/a\n/b`。命令执行是「一条消息一条」,首行之外的内容不会作为
|
|
55
|
+
* 独立命令执行。命中时返回首行之外的原始内容(即被忽略的部分),未命中返回 null。
|
|
56
|
+
* 绝对路径参数(如 `/cd /tmp`)与命令同处首行,不会误判。
|
|
57
|
+
*/
|
|
58
|
+
function findMergedCommandTail(message) {
|
|
59
|
+
const lines = message.split('\n');
|
|
60
|
+
if (lines.length < 2)
|
|
61
|
+
return null;
|
|
62
|
+
const tail = lines.slice(1).join('\n').trim();
|
|
63
|
+
if (!tail)
|
|
64
|
+
return null;
|
|
65
|
+
const hasSlashLine = lines.slice(1).some((line) => {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
return trimmed.length > 1 && trimmed.startsWith('/') && !trimmed.startsWith('/ ');
|
|
68
|
+
});
|
|
69
|
+
return hasSlashLine ? tail : null;
|
|
70
|
+
}
|
|
52
71
|
/** 人类可读字节数(B/KB/MB/GB);/ls 列表与单文件卡共用。 */
|
|
53
72
|
function formatBytes(bytes) {
|
|
54
73
|
if (bytes < 1024)
|
|
@@ -328,6 +347,8 @@ export class CommandRouter {
|
|
|
328
347
|
devMode;
|
|
329
348
|
/** Cache file for version checks (<configDir>/update-cache.json). */
|
|
330
349
|
updateCachePath;
|
|
350
|
+
/** 复制分身状态机(/clone 入口 + 活跃期消息拦截)。 */
|
|
351
|
+
cloneSession;
|
|
331
352
|
/** Injected update functions (for testability; defaults to real implementation). */
|
|
332
353
|
updateFns;
|
|
333
354
|
/** /config 卡片编辑暂存区(public:测试直接读取断言,替代 as unknown as)。 */
|
|
@@ -381,6 +402,7 @@ export class CommandRouter {
|
|
|
381
402
|
this.sessionReaderRegistry = opts.sessionReaderRegistry;
|
|
382
403
|
this.devMode = opts.devMode ?? false;
|
|
383
404
|
this.updateCachePath = opts.updateCachePath;
|
|
405
|
+
this.cloneSession = opts.cloneSession;
|
|
384
406
|
this.updateFns = {
|
|
385
407
|
checkLatestVersion: opts.updateFns?.checkLatestVersion ?? defaultCheckLatestVersion,
|
|
386
408
|
isNewer: opts.updateFns?.isNewer ?? defaultIsNewer,
|
|
@@ -402,6 +424,13 @@ export class CommandRouter {
|
|
|
402
424
|
const startsWithBang = trimmed.startsWith('!');
|
|
403
425
|
const startsWithSlash = trimmed.startsWith('/');
|
|
404
426
|
getLogger().info(`[router] handle message="${message.slice(0, 50)}..." trimmed="${trimmed.slice(0, 50)}..." startsWithSlash=${startsWithSlash} startsWithBang=${startsWithBang}`);
|
|
427
|
+
// 复制分身流程活跃期:一切消息(含命令)交给 clone 状态机处理,
|
|
428
|
+
// 不转发 coding agent、不进命令分发(index.ts 已在入队前拦截,这里
|
|
429
|
+
// 兜住 order.exec 等其他 router.handle 入口)。
|
|
430
|
+
if (this.cloneSession?.isActive()) {
|
|
431
|
+
await this.cloneSession.handleMessage(trimmed, ctx);
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
405
434
|
if (startsWithSlash) {
|
|
406
435
|
const result = await this.executeCommand(trimmed, ctx);
|
|
407
436
|
if (result) {
|
|
@@ -1929,6 +1958,16 @@ export class CommandRouter {
|
|
|
1929
1958
|
const parts = message.slice(1).split(/\s+/);
|
|
1930
1959
|
const cmd = parts[0]?.toLowerCase();
|
|
1931
1960
|
const args = parts.slice(1);
|
|
1961
|
+
// 飞书会把短间隔连发的消息合并成一条换行分隔的消息(见 findMergedCommandTail)。
|
|
1962
|
+
// 先提示被忽略的内容,再照常执行第一条命令,避免第二条被静默丢弃。
|
|
1963
|
+
const mergedTail = findMergedCommandTail(message);
|
|
1964
|
+
if (mergedTail) {
|
|
1965
|
+
await this.bridge.sendResult({
|
|
1966
|
+
text: `⚠️ 已忽略:${mergedTail}\n` +
|
|
1967
|
+
'一条消息只能执行一条命令\n' +
|
|
1968
|
+
'如果发得太快,也可能被飞书合并成一条消息',
|
|
1969
|
+
}, ctx);
|
|
1970
|
+
}
|
|
1932
1971
|
switch (cmd) {
|
|
1933
1972
|
case 'help':
|
|
1934
1973
|
case 'h':
|
|
@@ -1963,8 +2002,6 @@ export class CommandRouter {
|
|
|
1963
2002
|
return this.cmdResume(args, ctx);
|
|
1964
2003
|
case 'active':
|
|
1965
2004
|
return this.cmdActive(args, ctx);
|
|
1966
|
-
case 'reconnect':
|
|
1967
|
-
return await this.cmdReconnect();
|
|
1968
2005
|
case 'config':
|
|
1969
2006
|
case 'c':
|
|
1970
2007
|
return await this.cmdConfig(args, ctx);
|
|
@@ -1974,6 +2011,8 @@ export class CommandRouter {
|
|
|
1974
2011
|
case 'update':
|
|
1975
2012
|
case 'u':
|
|
1976
2013
|
return await this.cmdUpdate(args, ctx);
|
|
2014
|
+
case 'clone':
|
|
2015
|
+
return await this.cmdClone(args, ctx);
|
|
1977
2016
|
default:
|
|
1978
2017
|
return { text: `未知命令 /${cmd},输入 /help 查看可用命令` };
|
|
1979
2018
|
}
|
|
@@ -1994,8 +2033,12 @@ export class CommandRouter {
|
|
|
1994
2033
|
{ cmd: 'exit', label: '/exit /e', desc: '退出 bridge' },
|
|
1995
2034
|
{ cmd: 'restart', label: '/restart', desc: '重启 bridge(新进程,config 不变)' },
|
|
1996
2035
|
{ cmd: 'update', label: '/update /u', desc: '检查并升级到最新版本' },
|
|
2036
|
+
{
|
|
2037
|
+
cmd: 'clone',
|
|
2038
|
+
label: '/clone',
|
|
2039
|
+
desc: '复制分身:克隆配置并扫码创建新应用',
|
|
2040
|
+
},
|
|
1997
2041
|
{ cmd: 'status', label: '/status /s', desc: '显示当前状态' },
|
|
1998
|
-
{ cmd: 'reconnect', label: '/reconnect', desc: '重连飞书' },
|
|
1999
2042
|
].sort((a, b) => a.label.length - b.label.length || a.cmd.localeCompare(b.cmd));
|
|
2000
2043
|
// 文本组(需带参数,不宜用按钮;以纯文本行展示)
|
|
2001
2044
|
const textCommands = [
|
|
@@ -2024,9 +2067,9 @@ export class CommandRouter {
|
|
|
2024
2067
|
// 跨行不对齐(按钮列右边界参差)。改用 weighted 后,每行 column_set
|
|
2025
2068
|
// 总宽相同(同级 body.elements),weight 比例一致,列宽跨行恒等。
|
|
2026
2069
|
// 2026-07-05 手机排版修复:weight 1:3 → 2:3。1:3 时按钮列只占 25%,
|
|
2027
|
-
// 手机窄屏(~330px 可用)下约 82px,容不下 `/
|
|
2070
|
+
// 手机窄屏(~330px 可用)下约 82px,容不下 `/config /c`(10 字符 ≈ 100px),
|
|
2028
2071
|
// 按钮文字被截断成 "..."。改 2:3 后按钮列占 40% ≈ 132px,足够显示
|
|
2029
|
-
// 最长的 /
|
|
2072
|
+
// 最长的 /config /c,文本列仍占 60% 足以放下描述。
|
|
2030
2073
|
for (const c of buttonCommands) {
|
|
2031
2074
|
bodyElements.push({
|
|
2032
2075
|
tag: 'column_set',
|
|
@@ -2245,6 +2288,17 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2245
2288
|
// 4. Report success (no auto-restart — user decides when to /restart)
|
|
2246
2289
|
return { text: `✅ 升级成功 (${current} → ${latest}),发送 /restart 重启后生效` };
|
|
2247
2290
|
}
|
|
2291
|
+
/**
|
|
2292
|
+
* /clone [name] — 复制分身:进入 clone 状态机(发二维码、等扫码、自动绑定)。
|
|
2293
|
+
* 全程由 CloneSession 自己经 connector 发消息(含二维码图片),故返回 null。
|
|
2294
|
+
*/
|
|
2295
|
+
async cmdClone(args, ctx) {
|
|
2296
|
+
if (!this.cloneSession) {
|
|
2297
|
+
return { text: '当前环境未启用创建分身功能' };
|
|
2298
|
+
}
|
|
2299
|
+
await this.cloneSession.start(args[0], ctx);
|
|
2300
|
+
return null;
|
|
2301
|
+
}
|
|
2248
2302
|
cmdNew(ctx) {
|
|
2249
2303
|
// 只清 sessionId 保留 cwd — 否则 /new 之后 /resume 会提示"请先 /cd
|
|
2250
2304
|
// 设置工作目录",但用户的 workspace 还在。2026-06-21 复盘:用户
|
|
@@ -3470,15 +3524,6 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3470
3524
|
};
|
|
3471
3525
|
return { card };
|
|
3472
3526
|
}
|
|
3473
|
-
async cmdReconnect() {
|
|
3474
|
-
try {
|
|
3475
|
-
await this.bridge.reconnect();
|
|
3476
|
-
return { text: '已重连飞书' };
|
|
3477
|
-
}
|
|
3478
|
-
catch {
|
|
3479
|
-
return { text: '重连失败,请检查网络和配置' };
|
|
3480
|
-
}
|
|
3481
|
-
}
|
|
3482
3527
|
async cmdConfig(args, ctx) {
|
|
3483
3528
|
// Ensure probe cache is populated before building the card.
|
|
3484
3529
|
await probeAllAgents();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lark-remote",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"test:opencode-full": "vitest run --project agent-opencode --project infra-runner --project infra-session",
|
|
59
59
|
"test:pi-full": "vitest run --project agent-pi --project infra-runner --project infra-session",
|
|
60
60
|
"test:kimi-full": "vitest run --project agent-kimi --project infra-runner --project infra-session",
|
|
61
|
-
"test:live": "vitest run --project live",
|
|
61
|
+
"test:live": "node node_modules/vitest/vitest.mjs run --project live",
|
|
62
62
|
"prepublishOnly": "npm run typecheck && vitest run && npm run build",
|
|
63
63
|
"prepack": "node -e \"const fs=require('fs');if(!fs.existsSync('dist/cli.js')){console.error('dist/ missing — run bun run build first');process.exit(1)}\""
|
|
64
64
|
},
|