lark-remote 0.2.1 → 0.2.2
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 +13 -0
- package/README.en.md +0 -1
- package/README.md +0 -1
- 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/config/dir.js +15 -5
- package/dist/connector/index.d.ts +0 -1
- package/dist/connector/index.js +0 -6
- package/dist/platform/command.d.ts +2 -0
- package/dist/platform/command.js +54 -2
- package/dist/router/index.d.ts +0 -1
- package/dist/router/index.js +31 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.2] - 2026-09-12
|
|
9
|
+
|
|
10
|
+
### 修复
|
|
11
|
+
|
|
12
|
+
- **Windows `!` 命令修复**:解析 `bash` 时跳过 `system32` / `SysWOW64` 下的 WSL 启动器(那不是 Git Bash),并从 PATH 中的 `Git\cmd` 目录反推 `Git\bin\bash.exe` / `Git\usr\bin\bash.exe`;修复 `!` 命令被整条丢进 WSL 执行后因旧版 Node 解析不了新语法而报 `SyntaxError` 的问题
|
|
13
|
+
- **`lark-remote update` 不再被运行中的实例阻塞**:裸子命令 `update` / `version` / `help` 现在与 `--update` / `--version` / `--help` 等价;此前裸 `update` 会被当作位置参数落进守护进程路径去抢单例锁,被运行中的实例以 "already running" 挡下
|
|
14
|
+
|
|
15
|
+
### 变更
|
|
16
|
+
|
|
17
|
+
- **移除 `/reconnect` 命令**:路由分发、`/help` 按钮、Bridge 与 FeishuConnector 的重连实现及文档一并删除;重连需求已由 `/restart` 覆盖
|
|
18
|
+
- **消息合并提示**:飞书把极短间隔连发的多条消息合并成一条时,首行命令之后若还有以 `/` 开头的整行,会在执行第一条命令的同时提示被忽略的原始内容,不再静默丢弃
|
|
19
|
+
- **旧 Node 运行时友好化**:`dist/cli.js` 作为入口保持极旧 Node 可解析(避免 `??` / `?.` 语法),版本检查在加载应用模块图前由 preflight 处理;Node < 18 时给出明确的人话报错而非解析错误
|
|
20
|
+
|
|
8
21
|
## [0.2.1] - 2026-09-11
|
|
9
22
|
|
|
10
23
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -117,7 +117,6 @@ Your original message also gets an emoji reaction: `Typing` while the run is in
|
|
|
117
117
|
| `/status` | `/s` | Current directory, session, model, process status |
|
|
118
118
|
| `/stop` | `/t` | Kill the current agent process (SIGKILL) |
|
|
119
119
|
| `/ps` | - | Whether a process is running |
|
|
120
|
-
| `/reconnect` | - | Reconnect to Feishu |
|
|
121
120
|
| `/restart` | - | In-place bridge self-restart (new process takes over with the same config) |
|
|
122
121
|
| `/config get\|set` | `/c` | View/change runtime config (agent-aware card) |
|
|
123
122
|
| `/order save\|list\|edit` | `/o` | Save, list, or edit frequently used prompts |
|
package/README.md
CHANGED
|
@@ -117,7 +117,6 @@ bridge 启动后不在终端输出,运行日志写入 `~/.lark-remote/logs/YYY
|
|
|
117
117
|
| `/status` | `/s` | 当前目录、session、模型、进程状态 |
|
|
118
118
|
| `/stop` | `/t` | 终止当前 agent 进程(SIGKILL) |
|
|
119
119
|
| `/ps` | - | 是否有进程在跑 |
|
|
120
|
-
| `/reconnect` | - | 重连飞书 |
|
|
121
120
|
| `/restart` | - | 原地自重启 bridge(新进程同 config 接管) |
|
|
122
121
|
| `/config get\|set` | `/c` | 查改运行时配置(agent-aware 卡片) |
|
|
123
122
|
| `/order save\|list\|edit\|alias` | `/o` | 收藏常用指令;`/order edit` 修改指令文本(保留别名/使用统计);`/order alias` 注册快捷别名(输入 `$name` 展开) |
|
package/dist/bridge/index.d.ts
CHANGED
|
@@ -10,9 +10,8 @@ import type { InboundMediaPayload } from '../connector/index.js';
|
|
|
10
10
|
/**
|
|
11
11
|
* Seam (§7): the channel capabilities the bridge needs. `sendWithRetry` is
|
|
12
12
|
* the minimum a Feishu channel must offer for the bridge to push messages
|
|
13
|
-
* through it
|
|
14
|
-
*
|
|
15
|
-
* so tests can satisfy it structurally without `as unknown as` casts.
|
|
13
|
+
* through it. Declared here so tests can satisfy it structurally without
|
|
14
|
+
* `as unknown as` casts.
|
|
16
15
|
*/
|
|
17
16
|
interface BridgeChannel extends RunCardChannel {
|
|
18
17
|
sendWithRetry(chatId: string, input: {
|
|
@@ -24,7 +23,6 @@ interface BridgeChannel extends RunCardChannel {
|
|
|
24
23
|
}, opts?: {
|
|
25
24
|
replyTo?: string;
|
|
26
25
|
}): Promise<string>;
|
|
27
|
-
reconnect(): Promise<void>;
|
|
28
26
|
sendFile(chatId: string, filePath: string): Promise<string>;
|
|
29
27
|
addReaction(messageId: string, emoji: string): Promise<void>;
|
|
30
28
|
removeReactionByEmoji(messageId: string, emoji: string): Promise<void>;
|
|
@@ -326,8 +324,6 @@ export declare class Bridge {
|
|
|
326
324
|
flushMediaNotifications(userId: string, chatId: string): void;
|
|
327
325
|
/** 冲刷全部待合批提示(/exit、/restart 干净退出前调用)。 */
|
|
328
326
|
flushAllMediaNotifications(): Promise<void>;
|
|
329
|
-
/** Reconnect the underlying Feishu channel. */
|
|
330
|
-
reconnect(): Promise<void>;
|
|
331
327
|
/**
|
|
332
328
|
* Resolve the working directory for a user.
|
|
333
329
|
* First checks sessionStore (set by /cd), then falls back to first workspace.
|
package/dist/bridge/index.js
CHANGED
|
@@ -683,10 +683,6 @@ export class Bridge {
|
|
|
683
683
|
async flushAllMediaNotifications() {
|
|
684
684
|
await this.mediaHandler.flushAllPending();
|
|
685
685
|
}
|
|
686
|
-
/** Reconnect the underlying Feishu channel. */
|
|
687
|
-
async reconnect() {
|
|
688
|
-
await this.connector.reconnect();
|
|
689
|
-
}
|
|
690
686
|
/**
|
|
691
687
|
* Resolve the working directory for a user.
|
|
692
688
|
* First checks sessionStore (set by /cd), then falls back to first workspace.
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
type RuntimeChoice = 'bun' | 'node';
|
|
3
|
+
/**
|
|
4
|
+
* 纯信息类命令 + Node 版本守卫。
|
|
5
|
+
*
|
|
6
|
+
* 必须在 import 应用模块图之前执行。dist/cli.js 自身刻意不使用 ES2020+ 语法
|
|
7
|
+
* (?? / ?. / ??=),这样它在 node 12 上也 **能被解析**;而 dist/index.js 是
|
|
8
|
+
* ES2022,node<14 一加载就是 `SyntaxError: Unexpected token '?'`,堆栈全部落在
|
|
9
|
+
* Node 内部、完全看不出是版本问题(实测:WSL 里的 lark-remote 报的就是这个)。
|
|
10
|
+
*
|
|
11
|
+
* @returns 已处理完时返回退出码(0/1),调用方应立即 `process.exit(code)`;
|
|
12
|
+
* 返回 null 表示不是信息类命令、运行时也够新,继续正常启动。
|
|
13
|
+
*/
|
|
14
|
+
export declare function handlePreflight(args: string[], nodeVersion?: string): number | null;
|
|
3
15
|
interface BunProbe {
|
|
4
16
|
error: boolean;
|
|
5
17
|
status: number | null;
|
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,44 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { printHelp, printVersion } from './config/dir.js';
|
|
6
7
|
const entryUrl = new URL('./index.js', import.meta.url);
|
|
7
8
|
const entry = fileURLToPath(entryUrl);
|
|
9
|
+
/**
|
|
10
|
+
* 最低支持的 Node 主版本。package.json engines 写的是 >=20,这里放宽一档到
|
|
11
|
+
* 18 —— 守卫的目的是把「node 太老」变成一句人话,而不是把边界再收紧一次。
|
|
12
|
+
*/
|
|
13
|
+
const MIN_NODE_MAJOR = 18;
|
|
14
|
+
/**
|
|
15
|
+
* 纯信息类命令 + Node 版本守卫。
|
|
16
|
+
*
|
|
17
|
+
* 必须在 import 应用模块图之前执行。dist/cli.js 自身刻意不使用 ES2020+ 语法
|
|
18
|
+
* (?? / ?. / ??=),这样它在 node 12 上也 **能被解析**;而 dist/index.js 是
|
|
19
|
+
* ES2022,node<14 一加载就是 `SyntaxError: Unexpected token '?'`,堆栈全部落在
|
|
20
|
+
* Node 内部、完全看不出是版本问题(实测:WSL 里的 lark-remote 报的就是这个)。
|
|
21
|
+
*
|
|
22
|
+
* @returns 已处理完时返回退出码(0/1),调用方应立即 `process.exit(code)`;
|
|
23
|
+
* 返回 null 表示不是信息类命令、运行时也够新,继续正常启动。
|
|
24
|
+
*/
|
|
25
|
+
export function handlePreflight(args, nodeVersion = process.versions.node) {
|
|
26
|
+
for (const arg of args) {
|
|
27
|
+
if (arg === '-v' || arg === '--version' || arg === 'version') {
|
|
28
|
+
printVersion();
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
if (arg === '-h' || arg === '--help' || arg === 'help') {
|
|
32
|
+
printHelp();
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const major = parseInt(String(nodeVersion).split('.')[0], 10);
|
|
37
|
+
if (!Number.isNaN(major) && major > 0 && major < MIN_NODE_MAJOR) {
|
|
38
|
+
console.error(`lark-remote 需要 Node >= ${MIN_NODE_MAJOR}(当前 ${nodeVersion})。\n` +
|
|
39
|
+
'请升级 Node 后重试;若你在 WSL 里执行,注意 WSL 发行版的 node 与 Windows 侧的 node 是两套。');
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
8
44
|
/** Prefer bun unless we are already under bun or bun is not usable. */
|
|
9
45
|
export function decideRuntime(isBun, probe) {
|
|
10
46
|
if (!isBun && !probe.error && probe.status === 0)
|
|
@@ -52,7 +88,8 @@ export async function bootstrap(deps) {
|
|
|
52
88
|
}
|
|
53
89
|
}
|
|
54
90
|
else {
|
|
55
|
-
|
|
91
|
+
// 不用 `??`:cli.js 是入口,必须在 node<14 上也能被解析(见 handlePreflight)。
|
|
92
|
+
exit(typeof code === 'number' ? code : 1);
|
|
56
93
|
}
|
|
57
94
|
});
|
|
58
95
|
}
|
|
@@ -69,10 +106,17 @@ function isDirectEntry() {
|
|
|
69
106
|
return candidates.some((p) => pathToFileURL(p).href === import.meta.url);
|
|
70
107
|
}
|
|
71
108
|
function main() {
|
|
109
|
+
const args = process.argv.slice(2);
|
|
110
|
+
// 信息类命令 / 过旧运行时:在探测 bun、import 应用模块图之前先处理掉。
|
|
111
|
+
const preflight = handlePreflight(args);
|
|
112
|
+
if (preflight !== null) {
|
|
113
|
+
process.exit(preflight);
|
|
114
|
+
}
|
|
115
|
+
const isBunFlag = process.isBun;
|
|
72
116
|
const deps = {
|
|
73
117
|
entry,
|
|
74
|
-
args
|
|
75
|
-
isBun:
|
|
118
|
+
args,
|
|
119
|
+
isBun: isBunFlag === true,
|
|
76
120
|
probe: probeBun(),
|
|
77
121
|
importEntry: () => import(entryUrl.href),
|
|
78
122
|
spawnBun: (e, a) => spawn('bun', [e, ...a], { stdio: 'inherit', windowsHide: true }),
|
package/dist/config/dir.js
CHANGED
|
@@ -28,10 +28,10 @@ export function parseCliArgs(args = process.argv.slice(2)) {
|
|
|
28
28
|
const result = {};
|
|
29
29
|
for (let i = 0; i < args.length; i++) {
|
|
30
30
|
const arg = args[i];
|
|
31
|
-
if (arg === '-h' || arg === '--help') {
|
|
31
|
+
if (arg === '-h' || arg === '--help' || arg === 'help') {
|
|
32
32
|
result.help = true;
|
|
33
33
|
}
|
|
34
|
-
else if (arg === '-v' || arg === '--version') {
|
|
34
|
+
else if (arg === '-v' || arg === '--version' || arg === 'version') {
|
|
35
35
|
result.version = true;
|
|
36
36
|
}
|
|
37
37
|
else if (arg === '--config-dir' && i + 1 < args.length) {
|
|
@@ -52,7 +52,10 @@ export function parseCliArgs(args = process.argv.slice(2)) {
|
|
|
52
52
|
else if (arg === '--dev') {
|
|
53
53
|
result.dev = true;
|
|
54
54
|
}
|
|
55
|
-
else if (arg === '--update') {
|
|
55
|
+
else if (arg === '--update' || arg === 'update') {
|
|
56
|
+
// 裸子命令等价形式:`lark-remote update` 与 `--update` 同义。
|
|
57
|
+
// 不识别时它会落到守护进程路径去抢单例锁,被正在运行的实例挡下
|
|
58
|
+
// ("already running"),而 update 根本不是守护类命令。
|
|
56
59
|
result.update = true;
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -62,7 +65,9 @@ export function parseCliArgs(args = process.argv.slice(2)) {
|
|
|
62
65
|
export function getVersion() {
|
|
63
66
|
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
64
67
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
65
|
-
|
|
68
|
+
// 不用 `??`:本模块会被 dist/cli.js 静态引入,而 cli.js 必须在很旧的 Node
|
|
69
|
+
// (实测 WSL v12)上也 **能解析** —— node<14 不认识 ??,会直接 SyntaxError。
|
|
70
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
66
71
|
}
|
|
67
72
|
/** Print version to stdout. */
|
|
68
73
|
export function printVersion() {
|
|
@@ -74,7 +79,12 @@ export function printHelp() {
|
|
|
74
79
|
'lark-remote — 飞书私聊 ↔ 本地 Coding Agent CLI 桥接',
|
|
75
80
|
'',
|
|
76
81
|
'Usage:',
|
|
77
|
-
' lark-remote [options]',
|
|
82
|
+
' lark-remote [command] [options]',
|
|
83
|
+
'',
|
|
84
|
+
'Commands:',
|
|
85
|
+
' update 等同 --update:升级到最新版本后退出(不被运行中的实例阻塞)',
|
|
86
|
+
' version 等同 --version',
|
|
87
|
+
' help 等同 --help',
|
|
78
88
|
'',
|
|
79
89
|
'Options:',
|
|
80
90
|
' --config-dir <path> 自定义配置目录(默认 ~/.lark-remote,可用于同机多实例)',
|
|
@@ -114,7 +114,6 @@ export declare class FeishuConnector {
|
|
|
114
114
|
}): Promise<InboundMediaPayload>;
|
|
115
115
|
connect(): Promise<void>;
|
|
116
116
|
disconnect(): Promise<void>;
|
|
117
|
-
reconnect(): Promise<void>;
|
|
118
117
|
/**
|
|
119
118
|
* Send a message with automatic rate-limit retry (§9.5).
|
|
120
119
|
* Encounters rate_limited / feishuCode 99991400|99991401(SDK 归类为
|
package/dist/connector/index.js
CHANGED
|
@@ -288,12 +288,6 @@ export class FeishuConnector {
|
|
|
288
288
|
this.isConnected = false;
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
|
-
async reconnect() {
|
|
292
|
-
// disconnect() swallows its own errors internally, so no try/catch needed
|
|
293
|
-
// here — it always resolves.
|
|
294
|
-
await this.disconnect();
|
|
295
|
-
await this.connect();
|
|
296
|
-
}
|
|
297
291
|
/**
|
|
298
292
|
* Send a message with automatic rate-limit retry (§9.5).
|
|
299
293
|
* Encounters rate_limited / feishuCode 99991400|99991401(SDK 归类为
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { LaunchSpec } from './types.js';
|
|
2
2
|
export type { LaunchSpec } from './types.js';
|
|
3
|
+
/** 判断一个已命中的 bash.exe 是否只是 WSL 启动器(而非 Git Bash)。 */
|
|
4
|
+
export declare function isWslBashLauncher(file: string): boolean;
|
|
3
5
|
export interface ResolveOptions {
|
|
4
6
|
platform?: NodeJS.Platform;
|
|
5
7
|
/** PATH 环境变量注入(测试用);默认 process.env.PATH */
|
package/dist/platform/command.js
CHANGED
|
@@ -18,6 +18,33 @@ import fs from 'node:fs';
|
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { currentPlatform, isWin32 } from './select.js';
|
|
20
20
|
const DEFAULT_PATHEXT = ['.exe', '.bat', '.cmd'];
|
|
21
|
+
/**
|
|
22
|
+
* win32 的「假 bash」:`%SystemRoot%\system32\bash.exe`(SysWOW64 同理)不是
|
|
23
|
+
* Git Bash,而是 WSL 启动器(wsl.exe 的 bash 别名)。PATH 里 system32 通常
|
|
24
|
+
* 排在 Git\cmd 之前,按目录顺序命中它会把整条 `!` 命令丢进 WSL 发行版执行:
|
|
25
|
+
* 工作目录变成 /mnt/d/...、node 变成 WSL 里装的那个版本。实测本机 WSL node
|
|
26
|
+
* 为 v12,加载 dist/index.js 的 ES2022 语法(?? / ?.)直接
|
|
27
|
+
* `SyntaxError: Unexpected token '?'`,`!lark-remote -v` 因此失败。
|
|
28
|
+
*
|
|
29
|
+
* 因此解析 `bash` 时必须跳过它,继续在 PATH 中找真正的 Git Bash。
|
|
30
|
+
*
|
|
31
|
+
* 判定只看两层目录名(`<...>\system32\bash.exe`):Git Bash 装在
|
|
32
|
+
* `<Git>\bin` / `<Git>\usr\bin`,不会叫 system32;反过来把真正的 Git Bash
|
|
33
|
+
* 装在名为 system32 的目录里也可以忽略。
|
|
34
|
+
*/
|
|
35
|
+
const WSL_BASH_PARENT_DIRS = ['system32', 'syswow64'];
|
|
36
|
+
/** 判断一个已命中的 bash.exe 是否只是 WSL 启动器(而非 Git Bash)。 */
|
|
37
|
+
export function isWslBashLauncher(file) {
|
|
38
|
+
const normalized = file.replaceAll('/', '\\').toLowerCase();
|
|
39
|
+
if (path.win32.basename(normalized) !== 'bash.exe')
|
|
40
|
+
return false;
|
|
41
|
+
return WSL_BASH_PARENT_DIRS.includes(path.win32.basename(path.win32.dirname(normalized)));
|
|
42
|
+
}
|
|
43
|
+
/** 查询目标是 bash(`bash` 或显式 `bash.exe`)时才走上面的特例。 */
|
|
44
|
+
function isBashQuery(name) {
|
|
45
|
+
const lower = name.toLowerCase();
|
|
46
|
+
return lower === 'bash' || lower === 'bash.exe';
|
|
47
|
+
}
|
|
21
48
|
/**
|
|
22
49
|
* 缓存 TTL:probe 层(runner/probe.ts)5 分钟,本层更短——bridge 运行期间
|
|
23
50
|
* 用户新装/卸载 agent(典型场景:按 /config 不可用提示去安装)后,最迟
|
|
@@ -56,17 +83,42 @@ function existsWin32(file) {
|
|
|
56
83
|
return posixForm;
|
|
57
84
|
return null;
|
|
58
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Git for Windows 只把 `Git\cmd` 放进 PATH(里面没有 bash.exe),真正的
|
|
88
|
+
* bash 在 `Git\bin\bash.exe` / `Git\usr\bin\bash.exe`。PATH 里只暴露 cmd
|
|
89
|
+
* 目录时,从 cmd 反推安装根再找 bash。
|
|
90
|
+
*/
|
|
91
|
+
function resolveBashFromGitCmdDir(pathEnv) {
|
|
92
|
+
for (const dir of listPathDirs(pathEnv, 'win32')) {
|
|
93
|
+
if (path.win32.basename(dir).toLowerCase() !== 'cmd')
|
|
94
|
+
continue;
|
|
95
|
+
const root = path.win32.dirname(dir);
|
|
96
|
+
for (const rel of ['bin\\bash.exe', 'usr\\bin\\bash.exe']) {
|
|
97
|
+
const found = existsWin32(path.win32.join(root, rel));
|
|
98
|
+
if (found)
|
|
99
|
+
return { kind: 'direct', file: found };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
59
104
|
function resolveWin32(name, pathEnv, pathExt) {
|
|
60
105
|
const hasExtension = /\.[A-Za-z0-9]+$/.test(name);
|
|
106
|
+
const bashQuery = isBashQuery(name);
|
|
61
107
|
for (const dir of listPathDirs(pathEnv, 'win32')) {
|
|
62
108
|
const candidates = hasExtension ? [name] : pathExt.map((ext) => name + ext);
|
|
63
109
|
for (const candidate of candidates) {
|
|
64
110
|
const found = existsWin32(path.win32.join(dir, candidate));
|
|
111
|
+
if (!found)
|
|
112
|
+
continue;
|
|
113
|
+
// WSL 启动器不是 Git Bash:跳过,继续找后面的 PATH 目录
|
|
114
|
+
if (bashQuery && isWslBashLauncher(found))
|
|
115
|
+
continue;
|
|
65
116
|
// .cmd/.bat 垫片命中即报告垫片路径本身;执行语义由 cross-spawn 负责
|
|
66
|
-
|
|
67
|
-
return { kind: 'direct', file: found };
|
|
117
|
+
return { kind: 'direct', file: found };
|
|
68
118
|
}
|
|
69
119
|
}
|
|
120
|
+
if (bashQuery)
|
|
121
|
+
return resolveBashFromGitCmdDir(pathEnv);
|
|
70
122
|
return null;
|
|
71
123
|
}
|
|
72
124
|
function resolvePosix(name, pathEnv) {
|
package/dist/router/index.d.ts
CHANGED
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)
|
|
@@ -1929,6 +1948,16 @@ export class CommandRouter {
|
|
|
1929
1948
|
const parts = message.slice(1).split(/\s+/);
|
|
1930
1949
|
const cmd = parts[0]?.toLowerCase();
|
|
1931
1950
|
const args = parts.slice(1);
|
|
1951
|
+
// 飞书会把短间隔连发的消息合并成一条换行分隔的消息(见 findMergedCommandTail)。
|
|
1952
|
+
// 先提示被忽略的内容,再照常执行第一条命令,避免第二条被静默丢弃。
|
|
1953
|
+
const mergedTail = findMergedCommandTail(message);
|
|
1954
|
+
if (mergedTail) {
|
|
1955
|
+
await this.bridge.sendResult({
|
|
1956
|
+
text: `⚠️ 已忽略:${mergedTail}\n` +
|
|
1957
|
+
'一条消息只能执行一条命令\n' +
|
|
1958
|
+
'如果发得太快,也可能被飞书合并成一条消息',
|
|
1959
|
+
}, ctx);
|
|
1960
|
+
}
|
|
1932
1961
|
switch (cmd) {
|
|
1933
1962
|
case 'help':
|
|
1934
1963
|
case 'h':
|
|
@@ -1963,8 +1992,6 @@ export class CommandRouter {
|
|
|
1963
1992
|
return this.cmdResume(args, ctx);
|
|
1964
1993
|
case 'active':
|
|
1965
1994
|
return this.cmdActive(args, ctx);
|
|
1966
|
-
case 'reconnect':
|
|
1967
|
-
return await this.cmdReconnect();
|
|
1968
1995
|
case 'config':
|
|
1969
1996
|
case 'c':
|
|
1970
1997
|
return await this.cmdConfig(args, ctx);
|
|
@@ -1995,7 +2022,6 @@ export class CommandRouter {
|
|
|
1995
2022
|
{ cmd: 'restart', label: '/restart', desc: '重启 bridge(新进程,config 不变)' },
|
|
1996
2023
|
{ cmd: 'update', label: '/update /u', desc: '检查并升级到最新版本' },
|
|
1997
2024
|
{ cmd: 'status', label: '/status /s', desc: '显示当前状态' },
|
|
1998
|
-
{ cmd: 'reconnect', label: '/reconnect', desc: '重连飞书' },
|
|
1999
2025
|
].sort((a, b) => a.label.length - b.label.length || a.cmd.localeCompare(b.cmd));
|
|
2000
2026
|
// 文本组(需带参数,不宜用按钮;以纯文本行展示)
|
|
2001
2027
|
const textCommands = [
|
|
@@ -2024,9 +2050,9 @@ export class CommandRouter {
|
|
|
2024
2050
|
// 跨行不对齐(按钮列右边界参差)。改用 weighted 后,每行 column_set
|
|
2025
2051
|
// 总宽相同(同级 body.elements),weight 比例一致,列宽跨行恒等。
|
|
2026
2052
|
// 2026-07-05 手机排版修复:weight 1:3 → 2:3。1:3 时按钮列只占 25%,
|
|
2027
|
-
// 手机窄屏(~330px 可用)下约 82px,容不下 `/
|
|
2053
|
+
// 手机窄屏(~330px 可用)下约 82px,容不下 `/config /c`(10 字符 ≈ 100px),
|
|
2028
2054
|
// 按钮文字被截断成 "..."。改 2:3 后按钮列占 40% ≈ 132px,足够显示
|
|
2029
|
-
// 最长的 /
|
|
2055
|
+
// 最长的 /config /c,文本列仍占 60% 足以放下描述。
|
|
2030
2056
|
for (const c of buttonCommands) {
|
|
2031
2057
|
bodyElements.push({
|
|
2032
2058
|
tag: 'column_set',
|
|
@@ -3470,15 +3496,6 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3470
3496
|
};
|
|
3471
3497
|
return { card };
|
|
3472
3498
|
}
|
|
3473
|
-
async cmdReconnect() {
|
|
3474
|
-
try {
|
|
3475
|
-
await this.bridge.reconnect();
|
|
3476
|
-
return { text: '已重连飞书' };
|
|
3477
|
-
}
|
|
3478
|
-
catch {
|
|
3479
|
-
return { text: '重连失败,请检查网络和配置' };
|
|
3480
|
-
}
|
|
3481
|
-
}
|
|
3482
3499
|
async cmdConfig(args, ctx) {
|
|
3483
3500
|
// Ensure probe cache is populated before building the card.
|
|
3484
3501
|
await probeAllAgents();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lark-remote",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|