chat-devops 26.1.2 → 26.2.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.
Files changed (3) hide show
  1. package/README.md +103 -57
  2. package/dist/cli.js +548 -81
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,73 +1,104 @@
1
1
  # chat-devops
2
2
 
3
- 飞书消息收发测试项目。参考 [lark-coding-agent-bridge](https://github.com/zarazhangrui/feishu-claude-code-bridge),但**不对接 LLM**,仅做固定内容回复。
3
+ 飞书 / Lark 消息机器人:扫码创建应用,通过 WebSocket 接收消息,并桥接到本机 coding agent(Claude Code、Codex、Cursor、Pi、Alice)处理。
4
4
 
5
5
  ## 功能
6
6
 
7
- - 首次运行通过扫码创建 PersonalAgent 飞书应用(`@larksuite/channel` 的 `registerApp`)
8
- - 用户授权后通过 WebSocket 接收消息
9
- - 固定回复规则:
10
- - 收到 `ping` 回复 `pong`
11
- - 其他消息 → 回复 `hello world`(可在配置中修改 `preferences.defaultReply`)
12
- - 斜杠命令:
13
- - `/cmd <shell>` 在本机执行 shell 命令并返回输出(如 `/cmd pwd`);长命令会先在同一条消息显示「正在执行…」并刷新耗时,完成后原地替换为结果
14
- - `/help` — 显示命令帮助
7
+ - **应用初始化**:首次运行扫码创建 PersonalAgent 飞书应用(`@larksuite/channel` 的 `registerApp`),或传入已有 `app-id` / `app-secret`
8
+ - **Agent 桥接**:将用户消息交给本地 agent CLI,支持流式 markdown / 交互卡片回复
9
+ - **多 agent 切换**:运行时用 `/use claude|codex|cursor|pi|alice` 切换,无需重启
10
+ - **会话与工作目录**:按 chat(及话题群 thread)隔离 session 与 cwd,支持 `/new`、`/reset`、`/resume`
11
+ - **斜杠命令**:`/cmd`、`/cwd`、`/status`、`/stop`、`/send`、`/help` 等(详见下方)
12
+ - **后台 daemon**:`start` / `stop` / `restart` / `status`,支持 macOS launchd、Linux systemd、Windows 任务计划
13
+ - **无 agent 降级**:agent 未安装或不可用时,仍可使用 `/cmd`、`/status` 等命令;普通消息回退为固定文本(`ping` `pong`,其余 `defaultReply`)
15
14
 
16
15
  ## 环境要求
17
16
 
18
17
  - Node.js >= 20.12
18
+ - 至少安装一种 agent CLI(按需):
19
+ - Claude Code(`claude`)
20
+ - OpenAI Codex CLI(`codex`)
21
+ - Cursor Agent(`agent`)
22
+ - Pi(`pi`)
23
+ - Alice(`alice`)
19
24
 
20
25
  ## 快速开始
21
26
 
22
27
  ```bash
23
- # 安装依赖
28
+ # 安装依赖(开发)
24
29
  npm install
25
30
 
26
- # 开发运行(直接执行 TypeScript 源码,无需 build)
31
+ # 前台运行(监听 src/ 变更自动重启)
27
32
  npm run dev
28
- # 或单次运行
33
+
34
+ # 或单次前台运行
29
35
  npm start
30
36
  ```
31
37
 
32
- `npm run dev` 会监听 `src/` 变更并自动重启;改代码后保存即可,不用手动 build。
38
+ 全局安装后可直接使用 `chat-devops` 命令:
39
+
40
+ ```bash
41
+ npm install -g chat-devops
42
+ chat-devops run
43
+ ```
33
44
 
34
- 生产/发布前再打包:
45
+ 扫码完成后,配置保存到 `~/.chat-devops/config.json`。在飞书中找到应用,私聊或在群里 @ 机器人发消息即可。
46
+
47
+ 生产/发布前打包:
35
48
 
36
49
  ```bash
37
50
  npm run build
38
51
  npm run start:dist
39
52
  ```
40
53
 
41
- 扫码完成后,配置会保存到 `~/.chat-devops/config.json`。
42
-
43
- 在飞书中找到刚创建的应用,发私聊消息或在群里 @ 机器人:
54
+ ## CLI 命令
44
55
 
45
- - `ping` 收到 `pong`
46
- - 发任意其他内容 → 收到 `hello world`
56
+ | 命令 | 说明 |
57
+ |------|------|
58
+ | `chat-devops run` | 前台运行 bot |
59
+ | `chat-devops start` | 安装并以后台 daemon 启动 |
60
+ | `chat-devops stop` | 停止 daemon |
61
+ | `chat-devops restart` | 重启 daemon |
62
+ | `chat-devops status` | 查看 daemon 状态 |
63
+ | `chat-devops update` | 全局升级到最新版 |
47
64
 
48
- ## 使用已有应用
65
+ 常用选项:
49
66
 
50
67
  ```bash
51
- node bin/chat-devops.mjs run \
52
- --app-id cli_xxxxxxxxxxxx \
53
- --app-secret <your-secret> \
54
- --tenant feishu
68
+ chat-devops run --agent cursor # 指定 agent 类型
69
+ chat-devops run --agent disabled # 禁用 agent,仅命令模式
70
+ chat-devops run --app-id cli_xxx --app-secret <secret> --tenant feishu
71
+ chat-devops run --debug # Cursor agent 调试输出
55
72
  ```
56
73
 
57
- 国际版 Lark
74
+ 国际版 Lark 将 `--tenant` 设为 `lark`。
58
75
 
59
- ```bash
60
- node bin/chat-devops.mjs run --app-id cli_xxx --app-secret <secret> --tenant lark
61
- ```
76
+ ## 斜杠命令
62
77
 
63
- ## 配置
78
+ | 命令 | 说明 |
79
+ |------|------|
80
+ | `/help` | 显示帮助卡片 |
81
+ | `/status` | 当前 scope、cwd、session、agent、队列状态 |
82
+ | `/new` / `/reset` | 清空当前 chat 的 agent 会话 |
83
+ | `/new chat [name]` | 新建群并拉你进群,继承 cwd |
84
+ | `/resume` | 查看可恢复的会话 |
85
+ | `/cwd <path>` | 设置工作目录(绝对路径或 `~/...`) |
86
+ | `/cwd view` | 查看工作目录 |
87
+ | `/use claude\|codex\|cursor\|pi\|alice` | 切换 agent |
88
+ | `/cmd <shell>` / `$ <shell>` | 本机执行 shell;需交互输入时直接发下一条消息 |
89
+ | `/stop` | 中断正在运行的 agent 任务或 shell 命令 |
90
+ | `/send <path>` | 发送本地文件到当前会话 |
64
91
 
65
- 配置文件默认路径:`~/.chat-devops/config.json`
92
+ 以 `/` 开头的消息均为命令,不会交给 agent。群中默认需 @ 机器人才响应(私聊始终响应)。
93
+
94
+ ## 配置
66
95
 
67
- 可通过环境变量 `FEISHU_DEVOPS_HOME` 修改数据目录。
96
+ 默认路径:`~/.chat-devops/config.json`(可通过环境变量 `FEISHU_DEVOPS_HOME` 修改数据目录)。
68
97
 
69
98
  ```json
70
99
  {
100
+ "schemaVersion": 2,
101
+ "agentKind": "claude",
71
102
  "accounts": {
72
103
  "app": {
73
104
  "id": "cli_xxxxxxxxxxxx",
@@ -77,42 +108,57 @@ node bin/chat-devops.mjs run --app-id cli_xxx --app-secret <secret> --tenant lar
77
108
  },
78
109
  "preferences": {
79
110
  "requireMentionInGroup": true,
80
- "defaultReply": "hello world"
111
+ "defaultReply": "hello world",
112
+ "cmdEnabled": true,
113
+ "cmdTimeoutSeconds": 300,
114
+ "cmdProgressIntervalSeconds": 5,
115
+ "messageReply": "markdown",
116
+ "showToolCalls": true,
117
+ "maxConcurrentRuns": 10,
118
+ "runIdleTimeoutMinutes": 0,
119
+ "isolateTopicThreads": true,
120
+ "agentStopGraceMs": 5000,
121
+ "access": {
122
+ "allowedUsers": [],
123
+ "allowedChats": [],
124
+ "admins": []
125
+ }
126
+ },
127
+ "workspaces": {
128
+ "default": "/path/to/project"
81
129
  }
82
130
  }
83
131
  ```
84
132
 
85
- - `requireMentionInGroup`:群中是否必须 @ 机器人才响应(默认 `true`);私聊始终响应
86
- - `defaultReply`:非 `ping` 消息时的固定回复内容
87
- - `cmdEnabled`:是否允许 `/cmd`(默认 `true`)
88
- - `cmdTimeoutSeconds`:`/cmd` 超时秒数(默认 `300`,最大 `1800`)
89
- - `cmdProgressIntervalSeconds`:`/cmd` 执行中状态刷新间隔(默认 `5` 秒;设为 `0` 则只显示初始「正在执行」不周期性刷新,最大 `60`)
133
+ 主要配置项:
134
+
135
+ - `agentKind`:默认 agent(`claude` / `codex` / `cursor` / `pi` / `alice`)
136
+ - `requireMentionInGroup`:群中是否必须 @ 机器人才响应(默认 `true`)
137
+ - `defaultReply`:agent 禁用或不可用时的固定回复
138
+ - `messageReply`:agent 回复渲染模式 — `markdown`(流式卡片)、`card`(完整交互卡)、`text`(完成后一次性发送)
139
+ - `cmdEnabled` / `cmdTimeoutSeconds` / `cmdProgressIntervalSeconds`:`/cmd` 相关
140
+ - `maxConcurrentRuns`:全局最大并发 agent 任务数(默认 `10`)
141
+ - `isolateTopicThreads`:话题群是否按话题隔离 cwd / session(默认 `true`)
142
+ - `preferences.access`:允许的用户、群聊、管理员白名单(空数组表示不限制)
90
143
 
91
- > **安全提示**:`/cmd` 会在运行 bot 的本机执行任意 shell 命令,仅建议在可信环境使用。
144
+ > **安全提示**:`/cmd` 会在运行 bot 的本机执行任意 shell 命令,agent 也可能读写本地文件系统。仅建议在可信环境使用。
92
145
 
93
146
  ## 项目结构
94
147
 
95
148
  ```
96
149
  src/
97
- ├── bot/
98
- ├── wizard.ts # 扫码创建应用(来自 lark-coding-agent-bridge)
99
- ├── channel.ts # WebSocket 连接与消息处理
100
- │ └── reply.ts # 固定回复逻辑
101
- ├── cli/
102
- ├── index.ts # CLI 入口
103
- ├── start.ts # 启动流程
104
- │ └── bootstrap.ts # 配置引导
105
- ├── config/ # 配置读写
106
- ├── platform/ # 原子写入
107
- └── utils/
108
- └── feishu-auth.ts # 凭证校验
150
+ ├── agent/ # Claude / Codex / Cursor / Pi / Alice 适配器
151
+ ├── bot/ # WebSocket 通道、命令处理、agent 运行
152
+ ├── card/ # 飞书交互卡片与流式渲染
153
+ ├── cli/ # CLI 入口、启动、daemon 服务
154
+ ├── config/ # 配置读写与 schema
155
+ ├── daemon/ # launchd / systemd / schtasks 集成
156
+ ├── policy/ # 访问控制与工作目录策略
157
+ ├── runtime/ # agent run 执行器
158
+ ├── session/ # 会话持久化
159
+ └── workspace/ # 按 scope 的工作目录
109
160
  ```
110
161
 
111
- ## 与参考项目的差异
162
+ ## 参考
112
163
 
113
- | 参考项目 (lark-coding-agent-bridge) | 本项目 |
114
- |-------------------------------------|--------|
115
- | Claude/Codex agent 适配 | 无 LLM,固定文本回复 |
116
- | 多 profile、加密 keystore | 单配置文件,明文 secret(0600 权限) |
117
- | 流式卡片、slash 命令 | 简单 text 回复 |
118
- | lark-cli 预检、后台 daemon | 仅前台 `run` 命令 |
164
+ - [lark-coding-agent-bridge](https://github.com/zarazhangrui/feishu-claude-code-bridge)
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { Command } from "commander";
4
4
  // package.json
5
5
  var package_default = {
6
6
  name: "chat-devops",
7
- version: "26.1.2",
7
+ version: "26.2.0",
8
8
  description: "\u98DE\u4E66\u6D88\u606F bot\uFF1A\u626B\u7801\u521B\u5EFA\u5E94\u7528\uFF0C\u901A\u8FC7 Claude Code / Codex \u5904\u7406\u6D88\u606F",
9
9
  type: "module",
10
10
  bin: {
@@ -95,6 +95,9 @@ function getAgentStopGraceMs(cfg) {
95
95
  if (typeof raw !== "number" || !Number.isFinite(raw)) return 5e3;
96
96
  return Math.min(3e4, Math.max(100, Math.floor(raw)));
97
97
  }
98
+ function getIsolateTopicThreads(cfg) {
99
+ return cfg.preferences?.isolateTopicThreads !== false;
100
+ }
98
101
  function getRunIdleTimeoutMs(cfg) {
99
102
  const raw = cfg.preferences?.runIdleTimeoutMinutes;
100
103
  if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return void 0;
@@ -2036,7 +2039,7 @@ function getAgentPreflightDiagnostic(err) {
2036
2039
  function isAgentPreflightDiagnostic(input) {
2037
2040
  if (!input || typeof input !== "object") return false;
2038
2041
  const raw = input;
2039
- return typeof raw.code === "string" && raw.code.startsWith("agent-") && (raw.agentId === "claude" || raw.agentId === "codex" || raw.agentId === "cursor" || raw.agentId === "pi") && typeof raw.agentName === "string" && typeof raw.command === "string";
2042
+ return typeof raw.code === "string" && raw.code.startsWith("agent-") && (raw.agentId === "claude" || raw.agentId === "codex" || raw.agentId === "cursor" || raw.agentId === "pi" || raw.agentId === "alice") && typeof raw.agentName === "string" && typeof raw.command === "string";
2040
2043
  }
2041
2044
  function codeForSpawnError(err) {
2042
2045
  if (err.code === "ENOENT") return "agent-binary-not-found";
@@ -3427,6 +3430,354 @@ function isWindowsCommandNotFoundLine4(line) {
3427
3430
  return process.platform === "win32" && /is not recognized as an internal or external command|operable program or batch file/i.test(line);
3428
3431
  }
3429
3432
 
3433
+ // src/agent/alice/ag-ui.ts
3434
+ var AG_UI = {
3435
+ RUN_STARTED: "RUN_STARTED",
3436
+ RUN_FINISHED: "RUN_FINISHED",
3437
+ RUN_ERROR: "RUN_ERROR",
3438
+ TEXT_MESSAGE_CONTENT: "TEXT_MESSAGE_CONTENT",
3439
+ TEXT_MESSAGE_CHUNK: "TEXT_MESSAGE_CHUNK",
3440
+ REASONING_MESSAGE_CONTENT: "REASONING_MESSAGE_CONTENT",
3441
+ REASONING_MESSAGE_CHUNK: "REASONING_MESSAGE_CHUNK",
3442
+ THINKING_TEXT_MESSAGE_CONTENT: "THINKING_TEXT_MESSAGE_CONTENT",
3443
+ TOOL_CALL_START: "TOOL_CALL_START",
3444
+ TOOL_CALL_ARGS: "TOOL_CALL_ARGS",
3445
+ TOOL_CALL_END: "TOOL_CALL_END",
3446
+ TOOL_CALL_RESULT: "TOOL_CALL_RESULT"
3447
+ };
3448
+ function createAgUiStreamState() {
3449
+ return { doneEmitted: false, errored: false };
3450
+ }
3451
+ function* translateAgUiEvent(raw, state) {
3452
+ if (!raw || typeof raw !== "object") return;
3453
+ const evt = raw;
3454
+ const type = evt.type;
3455
+ if (!type) return;
3456
+ switch (type) {
3457
+ case AG_UI.RUN_STARTED: {
3458
+ const sessionId = evt.sessionId ?? evt.threadId;
3459
+ state.threadId = sessionId ?? evt.threadId;
3460
+ state.runId = evt.runId;
3461
+ if (sessionId) {
3462
+ yield {
3463
+ type: "system",
3464
+ threadId: evt.threadId ?? sessionId,
3465
+ sessionId
3466
+ };
3467
+ }
3468
+ return;
3469
+ }
3470
+ case AG_UI.TEXT_MESSAGE_CONTENT:
3471
+ case AG_UI.TEXT_MESSAGE_CHUNK: {
3472
+ if (typeof evt.delta === "string" && evt.delta) {
3473
+ yield { type: "text", delta: evt.delta };
3474
+ }
3475
+ return;
3476
+ }
3477
+ case AG_UI.REASONING_MESSAGE_CONTENT:
3478
+ case AG_UI.REASONING_MESSAGE_CHUNK:
3479
+ case AG_UI.THINKING_TEXT_MESSAGE_CONTENT: {
3480
+ if (typeof evt.delta === "string" && evt.delta) {
3481
+ yield { type: "thinking", delta: evt.delta };
3482
+ }
3483
+ return;
3484
+ }
3485
+ case AG_UI.TOOL_CALL_START: {
3486
+ if (evt.toolCallId && evt.toolCallName) {
3487
+ state.pendingToolCall = {
3488
+ id: evt.toolCallId,
3489
+ name: evt.toolCallName,
3490
+ argsDelta: ""
3491
+ };
3492
+ }
3493
+ return;
3494
+ }
3495
+ case AG_UI.TOOL_CALL_ARGS: {
3496
+ if (state.pendingToolCall && evt.toolCallId === state.pendingToolCall.id && evt.delta) {
3497
+ state.pendingToolCall.argsDelta += evt.delta;
3498
+ }
3499
+ return;
3500
+ }
3501
+ case AG_UI.TOOL_CALL_END: {
3502
+ yield* flushPendingToolCall(state);
3503
+ return;
3504
+ }
3505
+ case AG_UI.TOOL_CALL_RESULT: {
3506
+ if (evt.toolCallId) {
3507
+ yield {
3508
+ type: "tool_result",
3509
+ id: evt.toolCallId,
3510
+ output: typeof evt.content === "string" ? evt.content : JSON.stringify(evt.content ?? ""),
3511
+ isError: false
3512
+ };
3513
+ }
3514
+ return;
3515
+ }
3516
+ case AG_UI.RUN_ERROR: {
3517
+ state.errored = true;
3518
+ yield {
3519
+ type: "error",
3520
+ message: evt.message ?? "alice agent run failed",
3521
+ terminationReason: "failed"
3522
+ };
3523
+ return;
3524
+ }
3525
+ case AG_UI.RUN_FINISHED: {
3526
+ if (!state.doneEmitted && !state.errored) {
3527
+ state.doneEmitted = true;
3528
+ yield {
3529
+ type: "done",
3530
+ threadId: state.threadId,
3531
+ sessionId: state.threadId,
3532
+ terminationReason: "normal"
3533
+ };
3534
+ }
3535
+ return;
3536
+ }
3537
+ default:
3538
+ return;
3539
+ }
3540
+ }
3541
+ function* flushPendingToolCall(state) {
3542
+ const pending = state.pendingToolCall;
3543
+ if (!pending) return;
3544
+ state.pendingToolCall = void 0;
3545
+ let input = pending.argsDelta;
3546
+ if (pending.argsDelta) {
3547
+ try {
3548
+ input = JSON.parse(pending.argsDelta);
3549
+ } catch {
3550
+ input = pending.argsDelta;
3551
+ }
3552
+ }
3553
+ yield { type: "tool_use", id: pending.id, name: pending.name, input };
3554
+ }
3555
+
3556
+ // src/agent/alice/adapter.ts
3557
+ function isAliceDebugEnabled(explicit) {
3558
+ if (explicit === true) return true;
3559
+ if (explicit === false) return false;
3560
+ const raw = process.env.FEISHU_DEVOPS_ALICE_DEBUG ?? process.env.LARK_CHANNEL_ALICE_DEBUG;
3561
+ return raw === "1" || raw === "true";
3562
+ }
3563
+ var AliceAdapter = class {
3564
+ id = "alice";
3565
+ displayName = "Alice Agent";
3566
+ binary;
3567
+ larkChannel;
3568
+ debug;
3569
+ botIdentity;
3570
+ constructor(opts = {}) {
3571
+ this.binary = opts.binary ?? "alice";
3572
+ this.larkChannel = opts.larkChannel;
3573
+ this.debug = isAliceDebugEnabled(opts.debug);
3574
+ }
3575
+ setBotIdentity(identity) {
3576
+ this.botIdentity = identity;
3577
+ }
3578
+ async isAvailable() {
3579
+ return (await this.checkAvailability()).ok;
3580
+ }
3581
+ async checkAvailability() {
3582
+ return checkAgentAvailability({
3583
+ agentId: "alice",
3584
+ agentName: "Alice Agent",
3585
+ command: this.binary,
3586
+ binaryPath: this.binary,
3587
+ args: ["--version"]
3588
+ });
3589
+ }
3590
+ run(opts) {
3591
+ if (!opts.cwd) {
3592
+ throw new Error("cwd is required for AliceAdapter.run");
3593
+ }
3594
+ const stdinPrompt = prefixBridgeSystemPrompt(opts.prompt, this.botIdentity);
3595
+ const args = ["--mode", "json", "-p"];
3596
+ if (opts.sessionId) args.push("--resume", opts.sessionId);
3597
+ if (this.debug) args.push("--debug");
3598
+ if (this.debug) {
3599
+ console.log("[alice debug] spawn", {
3600
+ binary: this.binary,
3601
+ cwd: opts.cwd,
3602
+ args,
3603
+ promptChars: stdinPrompt.length,
3604
+ hasSession: Boolean(opts.sessionId),
3605
+ via: "stdin"
3606
+ });
3607
+ }
3608
+ const child = spawnProcess(this.binary, args, {
3609
+ cwd: opts.cwd,
3610
+ env: mergeProcessEnv(process.env, buildLarkChannelEnv(this.larkChannel)),
3611
+ stdio: ["pipe", "pipe", "pipe"]
3612
+ });
3613
+ log.info("agent", "spawn", {
3614
+ pid: child.pid ?? null,
3615
+ binary: this.binary,
3616
+ cwd: opts.cwd ?? process.cwd(),
3617
+ hasSession: Boolean(opts.sessionId),
3618
+ promptChars: stdinPrompt.length,
3619
+ via: "stdin",
3620
+ agent: "alice",
3621
+ args: formatSpawnArgsForLog(args)
3622
+ });
3623
+ log.info("agent", "spawn-env", { agent: "alice", ...summarizeAgentEnv(process.env) });
3624
+ const stderrChunks = [];
3625
+ let runtimeError = null;
3626
+ let stderrBuffer = "";
3627
+ child.stderr.on("data", (chunk) => {
3628
+ stderrChunks.push(chunk);
3629
+ stderrBuffer += chunk.toString("utf8");
3630
+ let nl = stderrBuffer.indexOf("\n");
3631
+ while (nl !== -1) {
3632
+ const line = stderrBuffer.slice(0, nl);
3633
+ stderrBuffer = stderrBuffer.slice(nl + 1);
3634
+ if (line.trim()) log.warn("agent", "stderr", { line, agent: "alice" });
3635
+ if (isWindowsCommandNotFoundLine5(line)) {
3636
+ runtimeError = new Error(`failed to spawn alice: ${line.trim()}`);
3637
+ child.stdout.destroy();
3638
+ child.kill();
3639
+ }
3640
+ nl = stderrBuffer.indexOf("\n");
3641
+ }
3642
+ });
3643
+ child.on("error", (err) => {
3644
+ runtimeError = err;
3645
+ });
3646
+ child.on("exit", (code, signal) => {
3647
+ log.info("agent", "exit", { pid: child.pid ?? null, code, signal, agent: "alice" });
3648
+ });
3649
+ child.stdin.on("error", (err) => {
3650
+ log.warn("agent", "stdin-error", { message: err.message, agent: "alice" });
3651
+ });
3652
+ child.stdin.end(stdinPrompt, "utf8");
3653
+ const stopGraceMs = opts.stopGraceMs ?? 5e3;
3654
+ return {
3655
+ runId: opts.runId,
3656
+ events: createEventStream5(child, stderrChunks, () => runtimeError, this.debug),
3657
+ async stop() {
3658
+ if (child.exitCode !== null || child.signalCode !== null) return;
3659
+ log.info("agent", "stop-sigterm", {
3660
+ pid: child.pid ?? null,
3661
+ graceMs: stopGraceMs,
3662
+ agent: "alice"
3663
+ });
3664
+ child.kill("SIGTERM");
3665
+ await new Promise((resolve4) => {
3666
+ const timer = setTimeout(() => {
3667
+ if (child.exitCode === null && child.signalCode === null) {
3668
+ log.warn("agent", "stop-sigkill", {
3669
+ pid: child.pid ?? null,
3670
+ graceMs: stopGraceMs,
3671
+ reason: "grace-period-expired",
3672
+ agent: "alice"
3673
+ });
3674
+ child.kill("SIGKILL");
3675
+ }
3676
+ resolve4();
3677
+ }, stopGraceMs);
3678
+ child.once("exit", () => {
3679
+ clearTimeout(timer);
3680
+ resolve4();
3681
+ });
3682
+ });
3683
+ },
3684
+ waitForExit(timeoutMs) {
3685
+ if (child.exitCode !== null || child.signalCode !== null) {
3686
+ return Promise.resolve(true);
3687
+ }
3688
+ return new Promise((resolve4) => {
3689
+ const onExit = () => {
3690
+ clearTimeout(timer);
3691
+ resolve4(true);
3692
+ };
3693
+ const timer = setTimeout(() => {
3694
+ child.removeListener("exit", onExit);
3695
+ resolve4(false);
3696
+ }, timeoutMs);
3697
+ child.once("exit", onExit);
3698
+ });
3699
+ }
3700
+ };
3701
+ }
3702
+ };
3703
+ async function* createEventStream5(child, stderrChunks, getError, debug) {
3704
+ if (!child.pid) {
3705
+ const err = getError();
3706
+ yield {
3707
+ type: "error",
3708
+ message: err ? `failed to spawn alice: ${err.message}` : "spawn returned no pid",
3709
+ terminationReason: "failed"
3710
+ };
3711
+ return;
3712
+ }
3713
+ const streamState = createAgUiStreamState();
3714
+ const stats = createStdoutJsonStats();
3715
+ for await (const parsed of readStdoutJsonLines(child.stdout, { agent: "alice", debug, stats })) {
3716
+ if (debug) {
3717
+ console.log("[alice debug] event", parsed);
3718
+ }
3719
+ yield* translateAgUiEvent(parsed, streamState);
3720
+ }
3721
+ log.info("agent", "stdout-summary", {
3722
+ agent: "alice",
3723
+ rawLines: stats.rawLines,
3724
+ parsedOk: stats.parsedOk,
3725
+ parseFailed: stats.parseFailed,
3726
+ exitCode: child.exitCode,
3727
+ signal: child.signalCode,
3728
+ ...stats.parseFailed > 0 ? { failedSamples: stats.failedSamples } : {}
3729
+ });
3730
+ if (stats.rawLines === 0) {
3731
+ log.warn("agent", "stdout-empty", {
3732
+ agent: "alice",
3733
+ exitCode: child.exitCode,
3734
+ signal: child.signalCode
3735
+ });
3736
+ }
3737
+ const earlyRuntimeError = getError();
3738
+ if (earlyRuntimeError && child.exitCode === null && child.signalCode === null) {
3739
+ yield {
3740
+ type: "error",
3741
+ message: `alice runtime error: ${earlyRuntimeError.message}`,
3742
+ terminationReason: "failed"
3743
+ };
3744
+ return;
3745
+ }
3746
+ const exitCode = await new Promise((resolve4) => {
3747
+ if (child.exitCode !== null || child.signalCode !== null) {
3748
+ resolve4(child.exitCode);
3749
+ } else {
3750
+ child.once("exit", (code) => resolve4(code));
3751
+ }
3752
+ });
3753
+ const runtimeError = getError();
3754
+ if (!streamState.doneEmitted && !streamState.errored && exitCode === 0) {
3755
+ yield {
3756
+ type: "done",
3757
+ threadId: streamState.threadId,
3758
+ sessionId: streamState.threadId,
3759
+ terminationReason: "normal"
3760
+ };
3761
+ } else if (exitCode !== 0 && exitCode !== null && !streamState.errored) {
3762
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
3763
+ const detail = stderr ? `: ${stderr.slice(0, 500)}` : "";
3764
+ yield {
3765
+ type: "error",
3766
+ message: `alice agent exited with code ${exitCode}${detail}`,
3767
+ terminationReason: "failed"
3768
+ };
3769
+ } else if (runtimeError && !streamState.errored) {
3770
+ yield {
3771
+ type: "error",
3772
+ message: `alice runtime error: ${runtimeError.message}`,
3773
+ terminationReason: "failed"
3774
+ };
3775
+ }
3776
+ }
3777
+ function isWindowsCommandNotFoundLine5(line) {
3778
+ return process.platform === "win32" && /is not recognized as an internal or external command|operable program or batch file/i.test(line);
3779
+ }
3780
+
3430
3781
  // src/config/permissions.ts
3431
3782
  var ACCESS_ORDER = {
3432
3783
  "read-only": 0,
@@ -3628,8 +3979,8 @@ function normalizeProfileConfig(input) {
3628
3979
  if (raw.schemaVersion !== 2) {
3629
3980
  throw new Error("profile schemaVersion must be 2");
3630
3981
  }
3631
- if (raw.agentKind !== "claude" && raw.agentKind !== "codex" && raw.agentKind !== "cursor" && raw.agentKind !== "pi") {
3632
- throw new Error("agentKind must be claude, codex, cursor, or pi");
3982
+ if (raw.agentKind !== "claude" && raw.agentKind !== "codex" && raw.agentKind !== "cursor" && raw.agentKind !== "pi" && raw.agentKind !== "alice") {
3983
+ throw new Error("agentKind must be claude, codex, cursor, pi, or alice");
3633
3984
  }
3634
3985
  const accounts = normalizeAccounts(raw.accounts);
3635
3986
  if (raw.agentKind === "codex" && !raw.codex) {
@@ -3773,7 +4124,7 @@ function numberOr(value, fallback) {
3773
4124
 
3774
4125
  // src/config/agent-config.ts
3775
4126
  function toProfileConfig(cfg) {
3776
- const agentKind = cfg.agentKind === "codex" ? "codex" : cfg.agentKind === "cursor" ? "cursor" : cfg.agentKind === "pi" ? "pi" : "claude";
4127
+ const agentKind = cfg.agentKind === "codex" ? "codex" : cfg.agentKind === "cursor" ? "cursor" : cfg.agentKind === "pi" ? "pi" : cfg.agentKind === "alice" ? "alice" : "claude";
3777
4128
  const base = {
3778
4129
  schemaVersion: 2,
3779
4130
  agentKind,
@@ -3874,6 +4225,14 @@ function createRuntimeAgent(cfg, opts = {}) {
3874
4225
  ...opts.cursorDebug === true ? { debug: true } : {}
3875
4226
  });
3876
4227
  }
4228
+ if (profileConfig.agentKind === "alice") {
4229
+ const command = process.env.FEISHU_DEVOPS_ALICE_BIN ?? process.env.LARK_CHANNEL_ALICE_BIN ?? "alice";
4230
+ return new AliceAdapter({
4231
+ binary: command,
4232
+ larkChannel,
4233
+ ...opts.cursorDebug === true ? { debug: true } : {}
4234
+ });
4235
+ }
3877
4236
  return new ClaudeAdapter({ larkChannel });
3878
4237
  }
3879
4238
  async function checkRuntimeAgentAvailability(agent) {
@@ -3905,6 +4264,10 @@ async function resolvePiBinaryPath() {
3905
4264
  const command = process.env.FEISHU_DEVOPS_PI_BIN ?? process.env.LARK_CHANNEL_PI_BIN ?? "pi";
3906
4265
  return resolveExecutablePath(command);
3907
4266
  }
4267
+ async function resolveAliceBinaryPath() {
4268
+ const command = process.env.FEISHU_DEVOPS_ALICE_BIN ?? process.env.LARK_CHANNEL_ALICE_BIN ?? "alice";
4269
+ return resolveExecutablePath(command);
4270
+ }
3908
4271
  function applyAgentKindToConfig(cfg, agentKind, codexBinaryPath) {
3909
4272
  const next = { ...cfg, agentKind };
3910
4273
  if (agentKind === "codex" && codexBinaryPath) {
@@ -4458,7 +4821,7 @@ var SessionCatalog = class {
4458
4821
  function normalizeEntry(input) {
4459
4822
  if (!input || typeof input !== "object") return void 0;
4460
4823
  const raw = input;
4461
- if (typeof raw.key !== "string" || typeof raw.scopeId !== "string" || raw.agentId !== "claude" && raw.agentId !== "codex" && raw.agentId !== "cursor" && raw.agentId !== "pi" || typeof raw.cwdRealpath !== "string" || typeof raw.policyFingerprint !== "string" || raw.status !== "active" && raw.status !== "archived" || typeof raw.updatedAt !== "number") {
4824
+ if (typeof raw.key !== "string" || typeof raw.scopeId !== "string" || raw.agentId !== "claude" && raw.agentId !== "codex" && raw.agentId !== "cursor" && raw.agentId !== "pi" && raw.agentId !== "alice" || typeof raw.cwdRealpath !== "string" || typeof raw.policyFingerprint !== "string" || raw.status !== "active" && raw.status !== "archived" || typeof raw.updatedAt !== "number") {
4462
4825
  return void 0;
4463
4826
  }
4464
4827
  return {
@@ -4478,15 +4841,15 @@ function matchesIdentity(entry, input) {
4478
4841
  return entry.scopeId === input.scopeId && entry.agentId === input.agentId && entry.cwdRealpath === input.cwdRealpath && entry.policyFingerprint === input.policyFingerprint && entry.key === sessionCatalogKey(input);
4479
4842
  }
4480
4843
  function isValidAgentEntry(entry) {
4481
- if (entry.agentId === "claude" || entry.agentId === "cursor" || entry.agentId === "pi") {
4844
+ if (entry.agentId === "claude" || entry.agentId === "cursor" || entry.agentId === "pi" || entry.agentId === "alice") {
4482
4845
  return Boolean(entry.sessionId) && !entry.threadId;
4483
4846
  }
4484
4847
  return Boolean(entry.threadId) && !entry.sessionId;
4485
4848
  }
4486
4849
  function assertAgentIdentity(input) {
4487
- if (input.agentId === "claude" || input.agentId === "cursor" || input.agentId === "pi") {
4850
+ if (input.agentId === "claude" || input.agentId === "cursor" || input.agentId === "pi" || input.agentId === "alice") {
4488
4851
  if (!input.sessionId || input.threadId) {
4489
- throw new Error("Claude/Cursor/Pi catalog entries require sessionId and must not include threadId");
4852
+ throw new Error("Claude/Cursor/Pi/Alice catalog entries require sessionId and must not include threadId");
4490
4853
  }
4491
4854
  return;
4492
4855
  }
@@ -4651,10 +5014,12 @@ var WorkspaceStore = class {
4651
5014
  // src/bot/active-cmd-sessions.ts
4652
5015
  var ActiveCmdSessions = class {
4653
5016
  sessions = /* @__PURE__ */ new Map();
5017
+ interruptedScopes = /* @__PURE__ */ new Set();
4654
5018
  register(scope, session) {
4655
5019
  if (this.sessions.has(scope)) {
4656
5020
  throw new Error(`cmd session already active for scope: ${scope}`);
4657
5021
  }
5022
+ this.interruptedScopes.delete(scope);
4658
5023
  this.sessions.set(scope, session);
4659
5024
  }
4660
5025
  get(scope) {
@@ -4675,10 +5040,17 @@ var ActiveCmdSessions = class {
4675
5040
  interrupt(scope) {
4676
5041
  const session = this.sessions.get(scope);
4677
5042
  if (!session) return false;
5043
+ this.interruptedScopes.add(scope);
4678
5044
  this.sessions.delete(scope);
4679
5045
  session.handle.kill();
4680
5046
  return true;
4681
5047
  }
5048
+ /** 读取并清除用户 /stop 终止标记(供最终输出文案使用)。 */
5049
+ consumeInterrupted(scope) {
5050
+ if (!this.interruptedScopes.has(scope)) return false;
5051
+ this.interruptedScopes.delete(scope);
5052
+ return true;
5053
+ }
4682
5054
  stopAll() {
4683
5055
  for (const scope of [...this.sessions.keys()]) {
4684
5056
  this.interrupt(scope);
@@ -4738,6 +5110,23 @@ function piCapability(profile) {
4738
5110
  }
4739
5111
  };
4740
5112
  }
5113
+ function aliceCapability(profile) {
5114
+ const maxAccess = profile?.permissions.maxAccess ?? "full";
5115
+ return {
5116
+ agentId: "alice",
5117
+ sessionKind: "alice-thread",
5118
+ promptInjection: "stdin-prefix",
5119
+ systemPrompt: BRIDGE_SYSTEM_PROMPT,
5120
+ supportsNativeHistory: true,
5121
+ callback: {
5122
+ marker: "__bridge_cb",
5123
+ legacyMarkers: []
5124
+ },
5125
+ permissions: {
5126
+ maxAccess
5127
+ }
5128
+ };
5129
+ }
4741
5130
  function codexCapability(profile) {
4742
5131
  const maxAccess = profile.permissions.maxAccess;
4743
5132
  return {
@@ -5213,27 +5602,6 @@ function footerLine(status) {
5213
5602
  return "_\u270D\uFE0F \u6B63\u5728\u8F93\u51FA\u2026_";
5214
5603
  }
5215
5604
 
5216
- // src/bot/cwd.ts
5217
- import { homedir as homedir3 } from "os";
5218
- import { isAbsolute as isAbsolute2 } from "path";
5219
- function expandTilde(p) {
5220
- if (p === "~") return homedir3();
5221
- if (p.startsWith("~/")) return `${homedir3()}${p.slice(1)}`;
5222
- return p;
5223
- }
5224
- function isAbsoluteOrTilde(p) {
5225
- return isAbsolute2(p) || p === "~" || p.startsWith("~/");
5226
- }
5227
- function chatScope(msg) {
5228
- return msg.chatId;
5229
- }
5230
- function storedCwd(workspaces, chatId) {
5231
- return workspaces.cwdFor(chatId);
5232
- }
5233
- function effectiveCwd(workspaces, chatId) {
5234
- return workspaces.cwdFor(chatId) ?? process.cwd();
5235
- }
5236
-
5237
5605
  // src/policy/fingerprint.ts
5238
5606
  import { createHash } from "crypto";
5239
5607
 
@@ -5368,7 +5736,7 @@ function reject(code, userVisible) {
5368
5736
 
5369
5737
  // src/policy/workspace.ts
5370
5738
  import { realpath, stat as stat2 } from "fs/promises";
5371
- import { homedir as homedir4, tmpdir } from "os";
5739
+ import { homedir as homedir3, tmpdir } from "os";
5372
5740
  import { basename as basename2, dirname as dirname7, resolve as resolve2 } from "path";
5373
5741
  async function resolveWorkingDirectory(requestedCwd) {
5374
5742
  const trimmed = requestedCwd.trim();
@@ -5401,7 +5769,7 @@ function classifyHighRiskWorkingDirectory(real, requestedCwd, tempRealpath) {
5401
5769
  if (real === dirname7(real)) {
5402
5770
  return reject2("filesystem-root", requestedCwd, "\u4E0D\u80FD\u628A\u6587\u4EF6\u7CFB\u7EDF\u6839\u76EE\u5F55\u8BBE\u4E3A\u5DE5\u4F5C\u76EE\u5F55\u3002");
5403
5771
  }
5404
- const home = resolve2(homedir4());
5772
+ const home = resolve2(homedir3());
5405
5773
  if (real === dirname7(home)) {
5406
5774
  return reject2("user-root", requestedCwd, "\u4E0D\u80FD\u628A\u7528\u6237\u76EE\u5F55\u6839\u8BBE\u4E3A\u5DE5\u4F5C\u76EE\u5F55\uFF0C\u8BF7\u9009\u62E9\u66F4\u5177\u4F53\u7684\u5B50\u76EE\u5F55\u3002");
5407
5775
  }
@@ -5476,7 +5844,7 @@ async function startRunFlow(input) {
5476
5844
  cwdRealpath: workspace.cwdRealpath,
5477
5845
  policyFingerprint: policy.policyFingerprint
5478
5846
  });
5479
- if (catalogEntry?.agentId === "claude" || catalogEntry?.agentId === "cursor" || catalogEntry?.agentId === "pi") {
5847
+ if (catalogEntry?.agentId === "claude" || catalogEntry?.agentId === "cursor" || catalogEntry?.agentId === "pi" || catalogEntry?.agentId === "alice") {
5480
5848
  sessionId = catalogEntry.sessionId;
5481
5849
  resumeFrom = sessionId;
5482
5850
  } else if (catalogEntry?.agentId === "codex") {
@@ -5484,7 +5852,7 @@ async function startRunFlow(input) {
5484
5852
  resumeFrom = threadId;
5485
5853
  }
5486
5854
  }
5487
- if (!resumeFrom && (input.capability.agentId === "claude" || input.capability.agentId === "cursor" || input.capability.agentId === "pi")) {
5855
+ if (!resumeFrom && (input.capability.agentId === "claude" || input.capability.agentId === "cursor" || input.capability.agentId === "pi" || input.capability.agentId === "alice")) {
5488
5856
  resumeFrom = input.sessions.resumeFor(input.scopeId, workspace.cwdRealpath);
5489
5857
  sessionId = resumeFrom;
5490
5858
  const stale = input.sessions.getRaw(input.scopeId);
@@ -5526,7 +5894,7 @@ async function startRunFlow(input) {
5526
5894
  }
5527
5895
  function recordRunSessionEvent(input) {
5528
5896
  if (input.event.type !== "system") return;
5529
- if ((input.capability.agentId === "claude" || input.capability.agentId === "cursor" || input.capability.agentId === "pi") && input.event.sessionId) {
5897
+ if ((input.capability.agentId === "claude" || input.capability.agentId === "cursor" || input.capability.agentId === "pi" || input.capability.agentId === "alice") && input.event.sessionId) {
5530
5898
  const cwdRealpath = input.event.cwd ?? input.policy.cwdRealpath;
5531
5899
  input.sessions.set(input.scopeId, input.event.sessionId, cwdRealpath);
5532
5900
  input.sessionCatalog?.upsertActive({
@@ -5560,7 +5928,7 @@ var BRIDGE_AGENT_INSTRUCTIONS = [
5560
5928
  function openAccess() {
5561
5929
  return { ok: true, reason: "allowed-user" };
5562
5930
  }
5563
- async function runAgentMessage(deps, msg) {
5931
+ async function runAgentMessage(deps, msg, scope) {
5564
5932
  const {
5565
5933
  channel,
5566
5934
  cfg,
@@ -5571,14 +5939,13 @@ async function runAgentMessage(deps, msg) {
5571
5939
  workspaces,
5572
5940
  activePolicyFingerprints
5573
5941
  } = deps;
5574
- const scope = chatScope(msg);
5575
5942
  const prompt = buildPrompt(msg, channel.botIdentity);
5576
5943
  log.info("prompt", "built", { promptChars: prompt.length });
5577
5944
  const sendOpts = {
5578
5945
  replyTo: msg.messageId,
5579
5946
  ...msg.threadId ? { replyInThread: true } : {}
5580
5947
  };
5581
- const capability = profileConfig.agentKind === "codex" ? codexCapability(profileConfig) : profileConfig.agentKind === "cursor" ? cursorCapability(profileConfig) : profileConfig.agentKind === "pi" ? piCapability(profileConfig) : claudeCapability(profileConfig);
5948
+ const capability = profileConfig.agentKind === "codex" ? codexCapability(profileConfig) : profileConfig.agentKind === "cursor" ? cursorCapability(profileConfig) : profileConfig.agentKind === "pi" ? piCapability(profileConfig) : profileConfig.agentKind === "alice" ? aliceCapability(profileConfig) : claudeCapability(profileConfig);
5582
5949
  const scopeContext = {
5583
5950
  source: "im",
5584
5951
  chatId: msg.chatId,
@@ -5954,6 +6321,9 @@ async function buildConfigForKind(cfg, kind) {
5954
6321
  if (kind === "pi") {
5955
6322
  return applyAgentKindToConfig(cfg, "pi");
5956
6323
  }
6324
+ if (kind === "alice") {
6325
+ return applyAgentKindToConfig(cfg, "alice");
6326
+ }
5957
6327
  return applyAgentKindToConfig(cfg, "claude");
5958
6328
  }
5959
6329
  function clearAgentSessionState(sessions, sessionCatalog) {
@@ -5974,6 +6344,7 @@ function agentLabel(kind) {
5974
6344
  if (kind === "codex") return "Codex CLI";
5975
6345
  if (kind === "cursor") return "Cursor Agent";
5976
6346
  if (kind === "pi") return "Pi Agent";
6347
+ if (kind === "alice") return "Alice Agent";
5977
6348
  return "Claude Code";
5978
6349
  }
5979
6350
 
@@ -6201,6 +6572,17 @@ function normalizeTerminalOutput(text) {
6201
6572
  const lf = text.replace(/\r\n/g, "\n");
6202
6573
  return normalizeCarriageReturns(stripAnsiCodes(lf));
6203
6574
  }
6575
+ var TerminalOutputBuffer = class {
6576
+ raw = "";
6577
+ append(chunk) {
6578
+ this.raw += decodeShellOutput(chunk);
6579
+ return normalizeTerminalOutput(this.raw);
6580
+ }
6581
+ appendText(text) {
6582
+ this.raw += text;
6583
+ return normalizeTerminalOutput(this.raw);
6584
+ }
6585
+ };
6204
6586
  var WINDOWS_CP_TO_ENCODING = {
6205
6587
  "65001": "utf-8",
6206
6588
  "936": "gb18030",
@@ -6252,11 +6634,13 @@ function writePtyLine(term, text) {
6252
6634
  term.write(`${line}\r`);
6253
6635
  }
6254
6636
  function attachOutputPump(child, onChunk) {
6637
+ const stdoutBuf = new TerminalOutputBuffer();
6638
+ const stderrBuf = new TerminalOutputBuffer();
6255
6639
  child.stdout?.on("data", (chunk) => {
6256
- onChunk("stdout", normalizeTerminalOutput(decodeShellOutput(chunk)));
6640
+ onChunk("stdout", stdoutBuf.append(chunk));
6257
6641
  });
6258
6642
  child.stderr?.on("data", (chunk) => {
6259
- onChunk("stderr", normalizeTerminalOutput(decodeShellOutput(chunk)));
6643
+ onChunk("stderr", stderrBuf.append(chunk));
6260
6644
  });
6261
6645
  }
6262
6646
  function childSpawnHandle(child) {
@@ -6291,8 +6675,8 @@ function spawnPipeShellCommand(command, opts) {
6291
6675
  setTimeout(() => child.kill("SIGKILL"), 500).unref();
6292
6676
  }, timeoutMs);
6293
6677
  attachOutputPump(child, (kind, text) => {
6294
- if (kind === "stdout") stdout += text;
6295
- else stderr += text;
6678
+ if (kind === "stdout") stdout = text;
6679
+ else stderr = text;
6296
6680
  opts.onOutput?.(kind, text);
6297
6681
  });
6298
6682
  child.on("close", (exitCode) => {
@@ -6341,8 +6725,8 @@ function spawnExpectShellCommand(command, expectPath, opts) {
6341
6725
  setTimeout(() => child.kill("SIGKILL"), 500).unref();
6342
6726
  }, timeoutMs);
6343
6727
  attachOutputPump(child, (kind, text) => {
6344
- if (kind === "stdout") stdout += text;
6345
- else stderr += text;
6728
+ if (kind === "stdout") stdout = text;
6729
+ else stderr = text;
6346
6730
  opts.onOutput?.(kind, text);
6347
6731
  });
6348
6732
  child.on("close", (exitCode) => {
@@ -6392,6 +6776,7 @@ function spawnPtyShellCommand(command, opts) {
6392
6776
  }
6393
6777
  });
6394
6778
  let stdout = "";
6779
+ const stdoutBuf = new TerminalOutputBuffer();
6395
6780
  let timedOut = false;
6396
6781
  const timer = setTimeout(() => {
6397
6782
  timedOut = true;
@@ -6399,9 +6784,8 @@ function spawnPtyShellCommand(command, opts) {
6399
6784
  setTimeout(() => term.kill("SIGKILL"), 500).unref();
6400
6785
  }, timeoutMs);
6401
6786
  term.onData((data) => {
6402
- const text = normalizeTerminalOutput(data);
6403
- stdout += text;
6404
- opts.onOutput?.("stdout", text);
6787
+ stdout = stdoutBuf.appendText(data);
6788
+ opts.onOutput?.("stdout", stdout);
6405
6789
  });
6406
6790
  term.onExit(({ exitCode }) => {
6407
6791
  clearTimeout(timer);
@@ -6435,8 +6819,8 @@ async function spawnShellCommand(command, opts = {}) {
6435
6819
  setTimeout(() => child.kill("SIGKILL"), 500).unref();
6436
6820
  }, timeoutMs);
6437
6821
  attachOutputPump(child, (kind, text) => {
6438
- if (kind === "stdout") stdout += text;
6439
- else stderr += text;
6822
+ if (kind === "stdout") stdout = text;
6823
+ else stderr = text;
6440
6824
  opts.onOutput?.(kind, text);
6441
6825
  });
6442
6826
  child.on("close", (exitCode) => {
@@ -6492,7 +6876,7 @@ function truncateShellOutput(stdout, stderr) {
6492
6876
  truncated: true
6493
6877
  };
6494
6878
  }
6495
- function formatShellResult(command, result) {
6879
+ function formatShellResult(command, result, opts) {
6496
6880
  const lines = [`$ ${command}`];
6497
6881
  const stdout = result.stdout.trimEnd();
6498
6882
  const stderr = result.stderr.trimEnd();
@@ -6507,10 +6891,12 @@ function formatShellResult(command, result) {
6507
6891
  lines.push("", `\`\`\`bash
6508
6892
  ${blockLines.join("\n")}
6509
6893
  \`\`\``);
6510
- } else if (result.exitCode === 0 && !result.timedOut) {
6894
+ } else if (result.exitCode === 0 && !result.timedOut && !opts?.interrupted) {
6511
6895
  lines.push("", "_(\u65E0\u8F93\u51FA)_");
6512
6896
  }
6513
- if (result.timedOut) {
6897
+ if (opts?.interrupted) {
6898
+ lines.push("", "_\uFF08\u7528\u6237\u7EC8\u6B62\uFF09_");
6899
+ } else if (result.timedOut) {
6514
6900
  lines.push("", "_\uFF08\u547D\u4EE4\u8D85\u65F6\uFF0C\u5DF2\u7EC8\u6B62\uFF09_");
6515
6901
  } else if (result.exitCode !== 0 && result.exitCode !== null) {
6516
6902
  lines.push("", `_\uFF08exit ${result.exitCode}\uFF09_`);
@@ -6523,9 +6909,17 @@ ${blockLines.join("\n")}
6523
6909
 
6524
6910
  // src/bot/cmd-runner.ts
6525
6911
  var OUTPUT_FLUSH_INTERVAL_MS = 300;
6526
- function formatRunningStatus(command, elapsedSec, partial, interactive = false) {
6912
+ function looksWaitingForInput(stdout, stderr) {
6913
+ const raw = stdout + stderr;
6914
+ const trimmed = raw.trimEnd();
6915
+ if (!trimmed) return false;
6916
+ if (/[:?>#$]\s*$/.test(trimmed)) return true;
6917
+ if (/\(\s*[yY]\s*\/\s*[nN]\s*\)\s*$/.test(trimmed)) return true;
6918
+ return false;
6919
+ }
6920
+ function formatRunningStatus(command, elapsedSec, partial, waitingForInput = false) {
6527
6921
  const elapsed = elapsedSec > 0 ? ` (${elapsedSec}s)` : "";
6528
- const lines = [`\u23F3 \u6B63\u5728\u6267\u884C\u2026${elapsed}`, "", `$ ${command}`];
6922
+ const lines = [`$ ${command}`];
6529
6923
  if (partial) {
6530
6924
  const stdout = partial.stdout.trimEnd();
6531
6925
  const stderr = partial.stderr.trimEnd();
@@ -6545,9 +6939,13 @@ ${blockLines.join("\n")}
6545
6939
  lines.push("", "_\uFF08\u8F93\u51FA\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD\uFF09_");
6546
6940
  }
6547
6941
  }
6548
- if (interactive && elapsedSec >= 2) {
6549
- lines.push("", "\u2328\uFE0F \u5982\u9700\u8F93\u5165\uFF0C\u76F4\u63A5\u53D1\u9001\u4E0B\u4E00\u6761\u6D88\u606F\uFF08/stop \u53D6\u6D88\uFF09");
6942
+ if (waitingForInput) {
6943
+ lines.push("", "\u2328\uFE0F \u9700\u8981\u4F60\u8F93\u5165");
6550
6944
  }
6945
+ lines.push(
6946
+ "",
6947
+ waitingForInput ? `\u23F3 \u7B49\u5F85\u8F93\u5165\u2026${elapsed}\uFF08/stop \u53D6\u6D88\uFF09` : `\u23F3 \u6B63\u5728\u6267\u884C\u2026${elapsed}\uFF08/stop \u53D6\u6D88\uFF09`
6948
+ );
6551
6949
  return lines.join("\n");
6552
6950
  }
6553
6951
  async function runShellWithProgress(command, cfg, onProgress, cwd, activeCmdSessions, scope) {
@@ -6568,7 +6966,8 @@ async function runShellWithProgress(command, cfg, onProgress, cwd, activeCmdSess
6568
6966
  const pushProgress = async (partial) => {
6569
6967
  if (finished) return;
6570
6968
  elapsedSec = Math.floor((Date.now() - started) / 1e3);
6571
- await onProgress(formatRunningStatus(command, elapsedSec, partial, interactive));
6969
+ const waitingForInput = partial ? looksWaitingForInput(partial.stdout, partial.stderr) : false;
6970
+ await onProgress(formatRunningStatus(command, elapsedSec, partial, waitingForInput));
6572
6971
  };
6573
6972
  const scheduleOutputFlush = () => {
6574
6973
  if (finished || outputFlushScheduled) return;
@@ -6597,8 +6996,8 @@ async function runShellWithProgress(command, cfg, onProgress, cwd, activeCmdSess
6597
6996
  timeoutMs: getCmdTimeoutMs(cfg),
6598
6997
  interactive,
6599
6998
  onOutput: (kind, chunk) => {
6600
- if (kind === "stdout") stdout += chunk;
6601
- else stderr += chunk;
6999
+ if (kind === "stdout") stdout = chunk;
7000
+ else stderr = chunk;
6602
7001
  scheduleOutputFlush();
6603
7002
  },
6604
7003
  onSpawn: (handle) => {
@@ -6606,7 +7005,8 @@ async function runShellWithProgress(command, cfg, onProgress, cwd, activeCmdSess
6606
7005
  activeCmdSessions.register(scope, { command, handle });
6607
7006
  }
6608
7007
  });
6609
- return formatShellResult(command, result);
7008
+ const interrupted = scope ? activeCmdSessions?.consumeInterrupted(scope) ?? false : false;
7009
+ return formatShellResult(command, result, { interrupted });
6610
7010
  } finally {
6611
7011
  finished = true;
6612
7012
  if (outputFlushTimer) clearTimeout(outputFlushTimer);
@@ -6617,6 +7017,57 @@ async function runShellWithProgress(command, cfg, onProgress, cwd, activeCmdSess
6617
7017
  }
6618
7018
  }
6619
7019
 
7020
+ // src/bot/cwd.ts
7021
+ import { homedir as homedir4 } from "os";
7022
+ import { isAbsolute as isAbsolute2 } from "path";
7023
+ function expandTilde(p) {
7024
+ if (p === "~") return homedir4();
7025
+ if (p.startsWith("~/")) return `${homedir4()}${p.slice(1)}`;
7026
+ return p;
7027
+ }
7028
+ function isAbsoluteOrTilde(p) {
7029
+ return isAbsolute2(p) || p === "~" || p.startsWith("~/");
7030
+ }
7031
+ function storedCwd(workspaces, chatId) {
7032
+ return workspaces.cwdFor(chatId);
7033
+ }
7034
+ function effectiveCwd(workspaces, chatId) {
7035
+ return workspaces.cwdFor(chatId) ?? process.cwd();
7036
+ }
7037
+
7038
+ // src/bot/scope.ts
7039
+ function scopeForMessage(msg, cfg) {
7040
+ if (!getIsolateTopicThreads(cfg) || !msg.threadId) {
7041
+ return msg.chatId;
7042
+ }
7043
+ if (msg.chatMode === "topic") {
7044
+ return `${msg.chatId}:${msg.threadId}`;
7045
+ }
7046
+ return msg.chatId;
7047
+ }
7048
+ async function scopeForCardAction(channel, chatId, messageId, cfg) {
7049
+ if (!getIsolateTopicThreads(cfg)) {
7050
+ return { scope: chatId };
7051
+ }
7052
+ const mode = await channel.getChatMode(chatId);
7053
+ if (mode !== "topic") {
7054
+ return { scope: chatId };
7055
+ }
7056
+ const threadId = await lookupMessageThreadId(channel, messageId);
7057
+ if (!threadId) {
7058
+ return { scope: chatId };
7059
+ }
7060
+ return { scope: `${chatId}:${threadId}`, threadId };
7061
+ }
7062
+ async function lookupMessageThreadId(channel, messageId) {
7063
+ try {
7064
+ const [parent] = await channel.fetchRawMessage(messageId);
7065
+ return parent?.thread_id;
7066
+ } catch {
7067
+ return void 0;
7068
+ }
7069
+ }
7070
+
6620
7071
  // src/bot/group.ts
6621
7072
  async function createBoundChat(opts) {
6622
7073
  const { channel, name, inviteOpenId, description } = opts;
@@ -6726,7 +7177,7 @@ async function tryHandleCmdInput(ctx) {
6726
7177
  }
6727
7178
  async function tryHandleCommand(ctx) {
6728
7179
  const input = normalizeCommandInput(ctx.msg);
6729
- const scope = ctx.scope || chatScope(ctx.msg);
7180
+ const scope = ctx.scope || scopeForMessage(ctx.msg, ctx.cfg);
6730
7181
  ctx.scope = scope;
6731
7182
  if (input.startsWith("$")) {
6732
7183
  await handleCmd(input.slice(1).trim(), ctx);
@@ -6916,7 +7367,7 @@ async function handleStatus(_args, ctx) {
6916
7367
  activeRun: Boolean(ctx.activeRuns?.get(ctx.scope)),
6917
7368
  queue: ctx.processPool?.snapshot(),
6918
7369
  scope: ctx.scope,
6919
- chatMode: ctx.msg.chatType === "p2p" ? "p2p" : "group"
7370
+ chatMode: ctx.msg.chatMode ?? (ctx.msg.chatType === "p2p" ? "p2p" : "group")
6920
7371
  });
6921
7372
  await sendCard(ctx, card);
6922
7373
  }
@@ -6938,7 +7389,7 @@ async function handleResume(args, ctx) {
6938
7389
  const entries = sessionId ? [
6939
7390
  {
6940
7391
  sessionId,
6941
- preview: isCodex ? "\u5F53\u524D Codex thread" : profile?.agentKind === "cursor" ? "\u5F53\u524D Cursor \u4F1A\u8BDD" : profile?.agentKind === "pi" ? "\u5F53\u524D Pi \u4F1A\u8BDD" : "\u5F53\u524D Claude \u4F1A\u8BDD",
7392
+ preview: isCodex ? "\u5F53\u524D Codex thread" : profile?.agentKind === "cursor" ? "\u5F53\u524D Cursor \u4F1A\u8BDD" : profile?.agentKind === "pi" ? "\u5F53\u524D Pi \u4F1A\u8BDD" : profile?.agentKind === "alice" ? "\u5F53\u524D Alice \u4F1A\u8BDD" : "\u5F53\u524D Claude \u4F1A\u8BDD",
6942
7393
  relTime: "\u5F53\u524D",
6943
7394
  current: true
6944
7395
  }
@@ -6966,7 +7417,7 @@ async function handleResumeUse(sessionId, ctx) {
6966
7417
  return;
6967
7418
  }
6968
7419
  ctx.sessions?.set(ctx.scope, sessionId, cwd);
6969
- const agentLabel2 = profile?.agentKind === "cursor" ? "Cursor" : profile?.agentKind === "pi" ? "Pi" : "Claude";
7420
+ const agentLabel2 = profile?.agentKind === "cursor" ? "Cursor" : profile?.agentKind === "pi" ? "Pi" : profile?.agentKind === "alice" ? "Alice" : "Claude";
6970
7421
  await replyMarkdown(ctx, `\u2713 \u5DF2\u6062\u590D ${agentLabel2} \u4F1A\u8BDD\uFF0C\u8BF7\u7EE7\u7EED\u53D1\u9001\u6D88\u606F\u3002`);
6971
7422
  }
6972
7423
  async function handleStop(_args, ctx) {
@@ -7019,7 +7470,7 @@ async function handleSwitch(args, ctx) {
7019
7470
  }
7020
7471
  const kind = args.trim().toLowerCase();
7021
7472
  if (!isSwitchableAgentKind(kind)) {
7022
- await replyText(ctx, "\u7528\u6CD5: `/use claude` | `/use codex` | `/use cursor` | `/use pi`");
7473
+ await replyText(ctx, "\u7528\u6CD5: `/use claude` | `/use codex` | `/use cursor` | `/use pi` | `/use alice`");
7023
7474
  return;
7024
7475
  }
7025
7476
  const current = ctx.runtime.fullCfg.agentKind ?? "claude";
@@ -7052,7 +7503,7 @@ async function handleSwitch(args, ctx) {
7052
7503
  );
7053
7504
  }
7054
7505
  function isSwitchableAgentKind(value) {
7055
- return value === "claude" || value === "codex" || value === "cursor" || value === "pi";
7506
+ return value === "claude" || value === "codex" || value === "cursor" || value === "pi" || value === "alice";
7056
7507
  }
7057
7508
  function resolveStatusAgentName(ctx) {
7058
7509
  if (ctx.runtime?.agentUnavailable) {
@@ -7078,6 +7529,9 @@ function runtimeAccessStatus(profile) {
7078
7529
  if (profile.agentKind === "pi") {
7079
7530
  return { label: "mode", value: "json" };
7080
7531
  }
7532
+ if (profile.agentKind === "alice") {
7533
+ return { label: "mode", value: "json" };
7534
+ }
7081
7535
  return {
7082
7536
  label: "sandbox",
7083
7537
  value: `${profile.sandbox.defaultMode}/${profile.sandbox.maxMode}`
@@ -7137,15 +7591,21 @@ async function handleCardAction(deps) {
7137
7591
  const payload = value;
7138
7592
  const cmd = typeof payload.cmd === "string" ? payload.cmd : "";
7139
7593
  if (!cmd) return;
7140
- log.info("cardAction", "cmd", { cmd, chatId: deps.evt.chatId });
7141
- const msg = makeFakeMsg(deps.evt);
7594
+ const { scope, threadId } = await scopeForCardAction(
7595
+ deps.channel,
7596
+ deps.evt.chatId,
7597
+ deps.evt.messageId,
7598
+ deps.runtime.fullCfg
7599
+ );
7600
+ log.info("cardAction", "cmd", { cmd, scope, chatId: deps.evt.chatId });
7601
+ const msg = makeFakeMsg(deps.evt, threadId);
7142
7602
  const ctx = {
7143
7603
  channel: deps.channel,
7144
7604
  cfg: deps.runtime.fullCfg,
7145
7605
  fullCfg: deps.runtime.fullCfg,
7146
7606
  profileConfig: deps.runtime.profileConfig,
7147
7607
  msg,
7148
- scope: chatScope(msg),
7608
+ scope,
7149
7609
  workspaces: deps.workspaces,
7150
7610
  activeRuns: deps.activeRuns,
7151
7611
  activeCmdSessions: deps.activeCmdSessions,
@@ -7176,11 +7636,12 @@ function composeArgs(sub, payload) {
7176
7636
  const arg = typeof payload.arg === "string" && payload.arg || typeof payload.name === "string" && payload.name || "";
7177
7637
  return arg ? `${sub} ${arg}` : sub;
7178
7638
  }
7179
- function makeFakeMsg(evt) {
7639
+ function makeFakeMsg(evt, threadId) {
7180
7640
  return {
7181
7641
  messageId: evt.messageId,
7182
7642
  chatId: evt.chatId,
7183
7643
  chatType: "p2p",
7644
+ ...threadId ? { threadId, chatMode: "topic" } : {},
7184
7645
  senderId: evt.operator.openId,
7185
7646
  senderName: evt.operator.name,
7186
7647
  content: "",
@@ -7252,7 +7713,8 @@ async function startChannel(cfg, startOpts = {}) {
7252
7713
  handshakeTimeoutMs: 8e3,
7253
7714
  httpTimeoutMs: 3e4,
7254
7715
  respectProxyEnv: true,
7255
- includeRawEvent: true
7716
+ includeRawEvent: true,
7717
+ resolveChatMode: true
7256
7718
  };
7257
7719
  const channel = createLarkChannel(opts);
7258
7720
  let consecutiveReconnects = 0;
@@ -7363,17 +7825,12 @@ async function startChannel(cfg, startOpts = {}) {
7363
7825
  }
7364
7826
  async function handleMessage(channel, cfg, msg, workspaces, ctx) {
7365
7827
  const preview = msg.content.length > 80 ? `${msg.content.slice(0, 80)}\u2026` : msg.content;
7366
- log2("info", "message-received", {
7367
- chatType: msg.chatType,
7368
- sender: msg.senderId,
7369
- preview,
7370
- mentionedBot: msg.mentionedBot
7371
- });
7828
+ log2("info", "message-received", msg);
7372
7829
  if (msg.chatType !== "p2p" && getRequireMentionInGroup(cfg) && !msg.mentionedBot) {
7373
7830
  log2("info", "skip-no-mention", { chatId: msg.chatId });
7374
7831
  return;
7375
7832
  }
7376
- const scope = chatScope(msg);
7833
+ const scope = scopeForMessage(msg, cfg);
7377
7834
  const cmdInputCtx = {
7378
7835
  channel,
7379
7836
  cfg,
@@ -7412,7 +7869,8 @@ async function handleMessage(channel, cfg, msg, workspaces, ctx) {
7412
7869
  workspaces,
7413
7870
  activePolicyFingerprints: ctx.activePolicyFingerprints
7414
7871
  },
7415
- msg
7872
+ msg,
7873
+ scope
7416
7874
  );
7417
7875
  return;
7418
7876
  }
@@ -7459,6 +7917,8 @@ async function runStart(opts) {
7459
7917
  fullCfg = applyAgentKindToConfig(fullCfg, "cursor");
7460
7918
  } else if (agentKind === "pi") {
7461
7919
  fullCfg = applyAgentKindToConfig(fullCfg, "pi");
7920
+ } else if (agentKind === "alice") {
7921
+ fullCfg = applyAgentKindToConfig(fullCfg, "alice");
7462
7922
  } else {
7463
7923
  fullCfg = applyAgentKindToConfig(fullCfg, "claude");
7464
7924
  }
@@ -7502,6 +7962,13 @@ async function runStart(opts) {
7502
7962
  } catch {
7503
7963
  console.log(`\u2713 Pi Agent: pi${opts.debug ? " (debug)" : ""}`);
7504
7964
  }
7965
+ } else if (agentKind === "alice") {
7966
+ try {
7967
+ const alicePath = await resolveAliceBinaryPath();
7968
+ console.log(`\u2713 Alice Agent: ${alicePath}${opts.debug ? " (debug)" : ""}`);
7969
+ } catch {
7970
+ console.log(`\u2713 Alice Agent: alice${opts.debug ? " (debug)" : ""}`);
7971
+ }
7505
7972
  } else {
7506
7973
  console.log(`\u2713 Codex CLI: ${fullCfg.codex?.binaryPath ?? "codex"}`);
7507
7974
  }
@@ -7593,11 +8060,11 @@ var runOptions = [
7593
8060
  ["--app-id <id>", "\u4F7F\u7528\u5DF2\u6709\u98DE\u4E66\u5E94\u7528\uFF08\u8DF3\u8FC7\u626B\u7801\u521B\u5EFA\uFF09"],
7594
8061
  ["--app-secret <secret>", "App Secret\uFF08\u914D\u5408 --app-id\uFF09"],
7595
8062
  ["--tenant <tenant>", "\u79DF\u6237\u57DF\u540D\uFF1Afeishu \u6216 lark\uFF08\u9ED8\u8BA4 feishu\uFF09"],
7596
- ["--agent <kind>", "agent \u7C7B\u578B\uFF1Aclaude / codex / cursor / pi / disabled\uFF08\u9ED8\u8BA4 claude\uFF09"],
8063
+ ["--agent <kind>", "agent \u7C7B\u578B\uFF1Aclaude / codex / cursor / pi / alice / disabled\uFF08\u9ED8\u8BA4 claude\uFF09"],
7597
8064
  ["--debug", "Cursor agent \u8C03\u8BD5\uFF1A\u6253\u5370 spawn args \u4E0E\u539F\u751F stream-json \u4E8B\u4EF6"]
7598
8065
  ];
7599
8066
  function parseRunOpts(opts) {
7600
- const agent = opts.agent === "claude" || opts.agent === "codex" || opts.agent === "cursor" || opts.agent === "pi" || opts.agent === "disabled" ? opts.agent : void 0;
8067
+ const agent = opts.agent === "claude" || opts.agent === "codex" || opts.agent === "cursor" || opts.agent === "pi" || opts.agent === "alice" || opts.agent === "disabled" ? opts.agent : void 0;
7601
8068
  return { ...opts, agent, debug: opts.debug === true };
7602
8069
  }
7603
8070
  program.command("run").description("\u524D\u53F0\u8FD0\u884C bot\uFF0C\u63A5\u6536\u6D88\u606F\u5E76\u901A\u8FC7 Claude Code / Codex / Cursor \u56DE\u590D").option(...runOptions[0]).option(...runOptions[1]).option(...runOptions[2]).option(...runOptions[3]).option(...runOptions[4]).option(...runOptions[5]).action(async (opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chat-devops",
3
- "version": "26.1.2",
3
+ "version": "26.2.0",
4
4
  "description": "飞书消息 bot:扫码创建应用,通过 Claude Code / Codex 处理消息",
5
5
  "type": "module",
6
6
  "bin": {