lark-remote 0.2.0 → 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 +24 -3
- package/README.en.md +5 -6
- package/README.md +5 -6
- package/dist/bridge/index.d.ts +2 -6
- package/dist/bridge/index.js +1 -5
- 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 -7
- package/dist/index.js +1 -1
- 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 +32 -15
- package/dist/session/claude/sessions.js +1 -1
- package/dist/session/dsh/sessions.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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
|
+
|
|
21
|
+
## [0.2.1] - 2026-09-11
|
|
22
|
+
|
|
23
|
+
### 新增
|
|
24
|
+
|
|
25
|
+
- **Windows 正式支持**:原生 Windows 支持已在真机完成验证,README / getting-started 的平台前置条件从「macOS / Linux,暂不支持 Windows」更新为 macOS / Linux / Windows 10+
|
|
26
|
+
|
|
27
|
+
### 变更
|
|
28
|
+
|
|
29
|
+
- **安全文档措辞更新**:README 安全警告与 `SECURITY.md` 反映 Claude 审批模式——`permissionMode` 可配置(默认 `bypassPermissions`,配置为其他值即启用飞书卡片交互式审批),不再表述为「固定 bypassPermissions」;审批卡片描述从 Codex 专属改为 Claude / Codex / Kimi 通用
|
|
30
|
+
|
|
8
31
|
## [0.2.0] - 2026-09-11
|
|
9
32
|
|
|
10
33
|
### 新增
|
|
@@ -62,7 +85,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
62
85
|
### 变更
|
|
63
86
|
|
|
64
87
|
- kimi ACP 能力声明:`fs` 保持关闭(走本地磁盘兜底,避免未实现的 `fs/*` RPC 报 `METHOD_NOT_FOUND`),`terminal` 打开
|
|
65
|
-
- 文档清理:移除设计文档中对内部文档名(`lessons-redlines`)与本地仓库路径(`/Users/.../lark-remote`)的引用
|
|
66
88
|
|
|
67
89
|
## [0.1.10] - 2026-08-22
|
|
68
90
|
|
|
@@ -212,7 +234,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
212
234
|
- `displayTitle`/`summary` 跳过 task-notification 注入
|
|
213
235
|
- 修复 `scanClaudeSessions` 对迁移到其他目录的会话的遗漏(S2)
|
|
214
236
|
- 修复 probe 生命周期、工厂回退、卡片回调等评审发现的问题(F1-F8)
|
|
215
|
-
- 清理引用已删除文档(lessons-redlines、resume-card-budget-failure-analysis)导致的失效链接
|
|
216
237
|
|
|
217
238
|
### 变更
|
|
218
239
|
|
|
@@ -221,7 +242,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
221
242
|
- 移除 `pnpm-lock.yaml`,统一使用 `bun.lock` 管理依赖
|
|
222
243
|
- 新增 husky lint-staged pre-commit / pre-push 钩子(提交前 lint + typecheck)
|
|
223
244
|
- 启用 npm provenance(构建产物可溯源)
|
|
224
|
-
- 移除 watchdog 相关用户文档及 `open-source-playbook` 等临时文档
|
|
225
245
|
|
|
226
246
|
### 测试
|
|
227
247
|
|
|
@@ -300,3 +320,4 @@ Initial release.
|
|
|
300
320
|
[0.1.6]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.6
|
|
301
321
|
[0.1.7]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.7
|
|
302
322
|
[0.1.8]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.8
|
|
323
|
+
[0.2.1]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.2.1
|
package/README.en.md
CHANGED
|
@@ -16,20 +16,20 @@ Designed for a single-user, peer-to-peer private chat. The agent's system prompt
|
|
|
16
16
|
|
|
17
17
|
## ⚠️ Security Warning (read first)
|
|
18
18
|
|
|
19
|
-
This tool turns Feishu messages directly into local agent executions. Claude
|
|
19
|
+
This tool turns Feishu messages directly into local agent executions. **Out of the box, agents run without approval prompts**: Claude defaults to `bypassPermissions` (no approval card), but that permission mode is **configurable** — set it to anything other than `bypassPermissions` (`default` / `acceptEdits` / `auto` / `dontAsk` / `plan`) to enable interactive approval, where high-risk actions wait for your confirmation on the Feishu card. Codex uses the **app-server approval mode** (default `approvalPolicy=on-request` — command execution requires your confirmation on the Feishu card, sandbox defaults to `workspace-write`); Kimi defaults to `manual` (per-action approval); other agents run with their CLI defaults. **In any mode**, agents can read and write the local directories you specify. You MUST:
|
|
20
20
|
|
|
21
21
|
- **Use it only in your own private (p2p) chat.** Never add the bot to any group chat; never let anyone else talk to it.
|
|
22
22
|
- Restrict the app's visibility to **yourself only** in the Feishu Open Platform.
|
|
23
23
|
- Don't run it on a machine holding data you can't afford to lose; prefer a dedicated user / machine / container.
|
|
24
24
|
- Keep credentials such as `appSecret` in your local `config.yaml` only — **never commit them anywhere**.
|
|
25
25
|
|
|
26
|
-
In
|
|
26
|
+
In approval mode (Claude, Codex, Kimi), an approval card arrives before a command runs — approve or reject it right in Feishu:
|
|
27
27
|
|
|
28
|
-

|
|
29
29
|
|
|
30
30
|
## Prerequisites
|
|
31
31
|
|
|
32
|
-
- OS: macOS / Linux
|
|
32
|
+
- OS: macOS / Linux / Windows 10+ (native Windows support is verified on real hardware, with behavior matching macOS/Linux)
|
|
33
33
|
- Node.js 20+ ([Bun](https://bun.sh/) for development)
|
|
34
34
|
- A Feishu custom app (either way): scan-to-create (a QR code pops up in the terminal on first launch; scan it with the Feishu app to create the app and write credentials automatically); or create one manually in the Open Platform — enable bot capability, subscribe to `im.message.receive_v1` and `card.action.trigger` via **long connection** (WebSocket, no public address needed), with at least the `im:message` scope.
|
|
35
35
|
- Claude Code CLI installed locally and logged in once in a terminal (`claude` → browser OAuth). Same idea for other agents (codex / opencode / pi / kimi): install the corresponding CLI first. DSH connects to a local DSH Web Host via HTTP+WebSocket (no local subprocess), default address `http://127.0.0.1:3080`.
|
|
@@ -70,7 +70,7 @@ defaultAgent: claude
|
|
|
70
70
|
claude:
|
|
71
71
|
model: claude-opus-4-8
|
|
72
72
|
effort: medium # low | medium | high | xhigh | max
|
|
73
|
-
#
|
|
73
|
+
permissionMode: bypassPermissions # Claude official --permission-mode: default | acceptEdits | auto | bypassPermissions | manual | dontAsk | plan (switchable via /config card; anything other than bypassPermissions enables approval)
|
|
74
74
|
stopGraceMs: 5000
|
|
75
75
|
|
|
76
76
|
logging:
|
|
@@ -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
|
@@ -16,20 +16,20 @@
|
|
|
16
16
|
|
|
17
17
|
## ⚠️ 安全警告(必读)
|
|
18
18
|
|
|
19
|
-
本工具会把飞书消息直接转为本地 agent
|
|
19
|
+
本工具会把飞书消息直接转为本地 agent 执行。**开箱默认是不弹审批的**:Claude 默认以 `bypassPermissions` 运行(无审批卡),但该权限模式**可配置**——把它改成 `bypassPermissions` 以外的值(`default` / `acceptEdits` / `auto` / `dontAsk` / `plan`)即启用交互式审批,高风险操作会在飞书卡片上等你确认;Codex 采用 **app-server 审批模式**(默认 `approvalPolicy=on-request`,执行命令需在飞书卡片上确认,沙箱默认 `workspace-write`);Kimi 默认 `manual`(逐条审批);其他 agent 以各自 CLI 的默认权限运行。**无论哪种模式**,agent 都能读写你指定的本地目录,请务必:
|
|
20
20
|
|
|
21
21
|
- **仅限你自己的私聊(p2p)使用**。不要把机器人拉进任何群聊,不要让任何其他人能与它对话。
|
|
22
22
|
- 在飞书开放平台把应用可见范围限制到**只有你自己**。
|
|
23
23
|
- 运行本工具的机器上不要存放与损失承受力不符的数据;建议在独立用户/机器/容器里运行。
|
|
24
24
|
- `appSecret` 等凭据只在本地 `config.yaml`,**不要提交到任何仓库**。
|
|
25
25
|
|
|
26
|
-
Codex
|
|
26
|
+
审批模式下(Claude、Codex、Kimi),执行命令前会收到审批卡片,可直接在飞书里允许或拒绝:
|
|
27
27
|
|
|
28
|
-

|
|
29
29
|
|
|
30
30
|
## 前置条件
|
|
31
31
|
|
|
32
|
-
- 操作系统:macOS / Linux
|
|
32
|
+
- 操作系统:macOS / Linux / Windows 10+(原生 Windows 支持已完成真机验证,行为与 macOS/Linux 对齐)
|
|
33
33
|
- Node.js 20+(开发时用 [Bun](https://bun.sh/))
|
|
34
34
|
- 飞书自建应用(二选一):扫码创建(首次启动终端弹二维码,飞书 App 扫码即自动创建并写入凭据);或手动在开放平台建应用——开启机器人能力,订阅 `im.message.receive_v1` 和 `card.action.trigger`,订阅方式选「长连接」(WebSocket,无需公网地址),权限至少 `im:message`。
|
|
35
35
|
- Claude Code CLI:本地安装并在终端完成一次登录(`claude` → 浏览器 OAuth)。使用其他 agent(codex / opencode / pi / kimi)同理,先装好对应 CLI;DSH 需本地 DSH Web Host 在跑(默认 `http://127.0.0.1:3080`),lark-remote 通过 HTTP+WebSocket 直连、不 spawn 本地子进程。
|
|
@@ -70,7 +70,7 @@ defaultAgent: claude
|
|
|
70
70
|
claude:
|
|
71
71
|
model: claude-opus-4-8
|
|
72
72
|
effort: medium # low | medium | high | xhigh | max
|
|
73
|
-
#
|
|
73
|
+
permissionMode: bypassPermissions # Claude 官方 --permission-mode:default | acceptEdits | auto | bypassPermissions | manual | dontAsk | plan(`/config` 卡片可切换;非 bypassPermissions 会启用审批)
|
|
74
74
|
stopGraceMs: 5000
|
|
75
75
|
|
|
76
76
|
logging:
|
|
@@ -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.
|
|
@@ -699,7 +695,7 @@ export class Bridge {
|
|
|
699
695
|
const workspaces = this.workspaceStore.list();
|
|
700
696
|
if (workspaces.length > 0) {
|
|
701
697
|
// NOTE: fallback uses insertion order, not sort preference — by design
|
|
702
|
-
// (
|
|
698
|
+
// (cwd fallback stays insertion-order for now)
|
|
703
699
|
cwd = workspaces[0].path;
|
|
704
700
|
getLogger().info(`[bridge] resolveCwd fallback cwd=${cwd} (first saved workspace)`);
|
|
705
701
|
}
|
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
|
@@ -130,7 +130,6 @@ export class FeishuConnector {
|
|
|
130
130
|
// 返回时,lark SDK 正常 resolve、@larksuite/channel 的 patchCard 丢弃返回值,
|
|
131
131
|
// 导致终态卡 patch 被业务层拒绝时全链路无日志无兜底。这里只观测不改行为——
|
|
132
132
|
// 不 throw、不重试,返回值原样透传。
|
|
133
|
-
// 详见 .adversarial-tdd/prompt-patchcard-business-code-observability.md
|
|
134
133
|
// Guard: unit-test mocks may omit rawClient; skip probe installation in that case.
|
|
135
134
|
const messageService = tryGetPatchService(this.channel);
|
|
136
135
|
if (messageService?.patch) {
|
|
@@ -289,12 +288,6 @@ export class FeishuConnector {
|
|
|
289
288
|
this.isConnected = false;
|
|
290
289
|
}
|
|
291
290
|
}
|
|
292
|
-
async reconnect() {
|
|
293
|
-
// disconnect() swallows its own errors internally, so no try/catch needed
|
|
294
|
-
// here — it always resolves.
|
|
295
|
-
await this.disconnect();
|
|
296
|
-
await this.connect();
|
|
297
|
-
}
|
|
298
291
|
/**
|
|
299
292
|
* Send a message with automatic rate-limit retry (§9.5).
|
|
300
293
|
* Encounters rate_limited / feishuCode 99991400|99991401(SDK 归类为
|
package/dist/index.js
CHANGED
|
@@ -438,7 +438,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
438
438
|
const workspaces = workspaceStore.list();
|
|
439
439
|
if (workspaces.length > 0) {
|
|
440
440
|
// NOTE: fallback uses insertion order, not sort preference — by design
|
|
441
|
-
// (
|
|
441
|
+
// (cwd fallback stays insertion-order for now)
|
|
442
442
|
workspace = workspaces[0].path;
|
|
443
443
|
}
|
|
444
444
|
}
|
|
@@ -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)
|
|
@@ -1029,7 +1048,7 @@ export class CommandRouter {
|
|
|
1029
1048
|
}
|
|
1030
1049
|
const resolvedTarget = path.resolve(targetPath);
|
|
1031
1050
|
// 与 ls.browse 对齐:目标只要存在且是目录即可切换(不再做子树/父级限制)。
|
|
1032
|
-
// 安全边界由 binder owner 认证兜底,见 docs/architecture/design.md §9.7 更新说明。
|
|
1051
|
+
// 安全边界由 binder owner 认证兜底,见 docs/zh/architecture/design.md §9.7 更新说明。
|
|
1033
1052
|
if (!fs.existsSync(resolvedTarget) || !fs.statSync(resolvedTarget).isDirectory()) {
|
|
1034
1053
|
await this.bridge.sendResult({ text: `路径无效: ${resolvedTarget}` }, ctx);
|
|
1035
1054
|
return;
|
|
@@ -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();
|
|
@@ -114,7 +114,7 @@ export function listClaudeSessions(cwd, opts = {}) {
|
|
|
114
114
|
const limit = opts.limit;
|
|
115
115
|
const projectsDir = opts.projectsDir ?? defaultProjectsDir();
|
|
116
116
|
const index = ensureSessionIndex(projectsDir);
|
|
117
|
-
// 首次调用延迟全量建索引;之后 5s
|
|
117
|
+
// 首次调用延迟全量建索引;之后 5s 按需节流增量刷新(拍板 3)。
|
|
118
118
|
index.refresh();
|
|
119
119
|
// 统一 parser 的完整 cwd 集合 → A→B→C 任意中间 cwd 都可列出(核心 bug 修复)。
|
|
120
120
|
// readdir 失败时索引保留旧快照(首次失败则为空),与旧扫描的 readdir 兜底等价。
|
|
@@ -24,7 +24,7 @@ import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
|
24
24
|
* CC-05: history 拉取上限。DSH session.history 的 maxMessages 同时限制返回条数,
|
|
25
25
|
* 若用它同时充当展示截断,>50 事件的会话累计 usage 会少算。这里拉取一个足够大的
|
|
26
26
|
* 上限保证 usage 累加覆盖整个会话;展示仍由 capEvents(opts?.maxEvents ?? 50) 截断。
|
|
27
|
-
* (若 DSH 支持分页 cursor
|
|
27
|
+
* (若 DSH 支持分页 cursor 可改成分页,当前用有界大拉取)
|
|
28
28
|
*/
|
|
29
29
|
const DSH_HISTORY_FETCH_LIMIT = 10_000;
|
|
30
30
|
/** Synchronous unary POST via curl (the reader interface is synchronous). */
|
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",
|