linco-connect 1.1.6 → 1.1.8

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 +105 -454
  2. package/package.json +1 -1
  3. package/src/agents/codex.js +83 -19
package/README.md CHANGED
@@ -1,209 +1,130 @@
1
- # linco
1
+ # Linco Connect
2
2
 
3
- Linco / Linco 对接 Claude Code Codex 的本机 Agent 连接器。
3
+ Linco Connect 是运行在用户电脑上的本机 Agent 连接器,用于把 Linco IM 消息转发给本机 Agent CLI,并把回复、工具调用、权限确认、附件和生成文件回传到 IM。
4
4
 
5
- 它运行在用户自己的电脑上,负责把 Linco IM 服务端的消息转发给本机 Agent CLI,并把 Agent 的回复、工具调用、权限确认、附件和生成文件回传给 IM。仓库内置的 Web 页面只是本地自测页面,不是正式 IM 前端。
5
+ 它不是鉴权服务,也不是托管式多租户服务。用户凭证保存在本机配置文件中,连接地址默认由程序内置;普通用户不需要在 README 或配置中手动填写服务端地址。
6
6
 
7
- ## 定位
7
+ ## 功能
8
8
 
9
- - 本插件是**本机 Agent 连接器**,不是鉴权服务,也不是托管式多租户服务。
10
- - Linco IM 服务端负责校验连接 query 中的 `token=appId:appSecret`。
11
- - 插件连接 IM 时会把本机配置的 `appId/appSecret` 拼成单个 `token` 查询参数传给服务端。
12
- - IM WebSocket 地址由代码内置,并按 Agent 类型区分:
13
-
14
- ```text
15
- Claude Code: wss://chat.ddjf.info/socket/ai/claude
16
- Codex: wss://chat.ddjf.info/socket/ai/codex
17
- ```
18
-
19
- 普通用户初始化时不需要填写 `wsUrl`,私有化部署可在 `.linco/config.json` 中覆盖。
20
-
21
- ## 功能特性
22
-
23
- - npm CLI 安装和启动:支持 `linco-connect init/start/stop/doctor`,可用 `start --daemon` 后台运行。
24
- - 远端 IM 默认按配置连接,用户只需配置 `appId/appSecret`。
25
- - 本地测试页的模拟前端 IM 默认不连接:需要本地页面联调时需显式添加 `--local-im` 或 `--mock-im`。
26
- - 内置 Linco IM WebSocket 地址:用户只需配置 `appId/appSecret`,插件会在连接时拼成 `token=appId:appSecret`。
27
- - 多 Agent 连接:一个进程可同时连接 Claude Code 和 Codex 对应的 Linco 端点。
28
- - 本地自测页面:用于调试本机 Agent 桥接能力。
29
- - 本地访问 token:首次启动自动生成 `localToken`,保护本地测试页和本地 WebSocket。
30
- - Claude Code 流式桥接:使用 `stream-json` 输入/输出格式实时推送回复。
31
- - Codex 桥接:第一版使用系统安装的 `codex exec --json`,沿用系统 Codex 登录和配置。
32
- - 会话隔离:每个 Agent 和远端 session 都创建独立 session、workspace、attachments、outbox。
33
- - Markdown 展示:支持 GFM Markdown、代码块、表格、引用和安全链接。
34
- - 工具调用展示:Agent 工具输入、输出和错误以可折叠卡片展示。
35
- - 权限确认:工具/命令权限请求和危险操作确认默认自动允许;可用 `/approve off` 为当前会话恢复手动确认。
36
- - 附件上传:支持普通文件和图片;图片作为多模态内容发送,普通文件保存为本地路径引用。
37
- - 图片粘贴:支持从剪贴板粘贴图片。
38
- - 文件下发:Agent 生成文件放入 outbox 后,前端自动展示下载/预览卡片。
39
- - 斜杠命令:内置 `/help`、`/pwd`、`/cd`、`/new`、`/stop`、`/base`、`/status`、`/list`、`/approve`。
40
- - Windows Git Bash 检测:Windows 下自动查找 Git Bash,也可手动指定。
9
+ - 支持 Claude Code、Codex、Hermes、OpenClaw 等 Agent
10
+ - 支持远端 IM 连接,也支持本地测试页调试。
11
+ - 支持文本、图片和常见文档附件上传。
12
+ - 支持 Agent 生成文件自动下发:把文件放入当前会话的 outbox 目录即可推送给前端。
13
+ - 支持工具/命令权限确认和危险操作确认,默认自动审批,可在会话内切换。
14
+ - 支持会话历史查看、切换、删除和 Token 用量查看。
41
15
 
42
16
  ## 前置条件
43
17
 
44
18
  - Node.js 18+
45
- - 已安装 Claude Code CLI,终端可执行 `claude`
46
- - Claude Code 已完成登录或 API Key 配置
47
- - 如需启用 Codex:已安装 Codex CLI,终端可执行 `codex`,并已完成系统 Codex 登录或配置
48
- - Windows 用户建议安装 Git for Windows
19
+ - 已安装需要使用的 Agent CLI,并完成对应登录或配置:
20
+ - `claude`
21
+ - `codex`
22
+ - `hermes`
23
+ - `openclaw`
24
+ - Windows 用户建议安装 Git for Windows。
25
+
26
+ 只需要安装实际启用的 Agent。
49
27
 
50
28
  ## 安装
51
29
 
52
- 开发源码运行:
30
+ 源码运行:
53
31
 
54
32
  ```bash
55
33
  npm install
56
34
  npm start
57
35
  ```
58
36
 
59
- 作为 npm CLI 使用时:
37
+ 作为 CLI 使用:
60
38
 
61
39
  ```bash
62
40
  npm install -g linco-connect
63
- linco-connect init --token "appId:appSecret" --agent claude
64
- linco-connect doctor
65
- linco-connect start
66
- # 后台运行
67
- linco-connect start --daemon
68
- # 停止后台服务
69
- linco-connect stop
70
41
  ```
71
42
 
72
- 本地开发也可以用:
43
+ 本地源码也可以直接运行 CLI:
73
44
 
74
45
  ```bash
75
- node bin/linco-connect.js init --token "appId:appSecret" --agent claude
76
- node bin/linco-connect.js doctor
77
- node bin/linco-connect.js start
46
+ node bin/linco-connect.js --help
78
47
  ```
79
48
 
80
- ## 初始化配置
49
+ ## 初始化
81
50
 
82
- 推荐方式:
51
+ 推荐使用 token 初始化:
83
52
 
84
53
  ```bash
85
- linco-connect init --token "appId:appSecret" --agent claude
54
+ linco-connect init --token "<appId>:<appSecret>" --agent claude
86
55
  ```
87
56
 
88
- 可选参数:
57
+ 也可以分别传入:
89
58
 
90
59
  ```bash
91
- linco-connect init \
92
- --token "appId:appSecret" \
93
- --agent claude \
94
- --account default \
95
- --force
60
+ linco-connect init --app-id "<appId>" --app-secret "<appSecret>" --agent codex
96
61
  ```
97
62
 
98
- 说明:
63
+ 常用参数:
99
64
 
100
- - `--token` 格式为 `appId:appSecret`。
101
- - `--agent` 必填,指定 Agent 类型(如 `claude` 或 `codex`)。
102
- - token 按第一个冒号切分,所以 `appSecret` 内部可以包含冒号。
103
- - `--force` 用于覆盖已有账号配置。
104
- - 初始化不会询问或写入 `wsUrl`。
105
- - 多次执行 `init` 并传入不同 `--agent` 可在同一账号下启用多个 Agent。
65
+ | 参数 | 说明 |
66
+ | --- | --- |
67
+ | `--agent <类型>` | 启用指定 Agent,如 `claude`、`codex`、`hermes`、`openclaw` |
68
+ | `--account <名称>` | 指定账号名,默认 `default` |
69
+ | `--force` | 覆盖已有配置 |
106
70
 
107
- ### Hermes Agent 初始化
71
+ 如需启用多个 Agent,可多次执行 `init`,每次传入不同的 `--agent`。
108
72
 
109
- Hermes 使用本机 Hermes Gateway。默认情况下,`ddchat-connect` 会在第一次 Hermes 对话前自动检查 Gateway;如果未运行,会参考 Hermes Web UI 的方式自动补齐当前 Hermes profile 的 `platforms.api_server` 配置,生成本机 API key,并执行 `hermes gateway run --replace`。
73
+ ## 启动和停止
110
74
 
111
- 最小配置只需要启用 Hermes:
75
+ 前台启动:
112
76
 
113
- ```json
114
- {
115
- "agents": {
116
- "hermes": {
117
- "enabled": true
118
- }
119
- }
120
- }
77
+ ```bash
78
+ linco-connect start
121
79
  ```
122
80
 
123
- 如需覆盖默认值,可配置:
81
+ 后台启动:
124
82
 
125
- ```json
126
- {
127
- "agents": {
128
- "hermes": {
129
- "enabled": true,
130
- "gatewayUrl": "http://127.0.0.1:8642",
131
- "autoStartGateway": true,
132
- "hermesBin": "hermes",
133
- "profile": "default"
134
- }
135
- }
136
- }
83
+ ```bash
84
+ linco-connect start --daemon
137
85
  ```
138
86
 
139
- 如果已经有外部 Gateway,可关闭自动启动:
87
+ 停止后台服务:
140
88
 
141
- ```json
142
- {
143
- "agents": {
144
- "hermes": {
145
- "enabled": true,
146
- "gatewayUrl": "http://127.0.0.1:9000",
147
- "autoStartGateway": false
148
- }
149
- }
150
- }
89
+ ```bash
90
+ linco-connect stop
151
91
  ```
152
92
 
153
- 可通过 `linco-connect doctor` 检查 Hermes Gateway 状态;Gateway 未运行但自动启动开启时,发送 Hermes 消息会自动启动。
93
+ 检查本机环境:
154
94
 
155
- #### 接口鉴权
156
-
157
- 如果 Hermes 的 `api_server` 配置了 `key`(接口鉴权),需要在 `~/.linco/config.json` 中添加对应的 API Key:
158
-
159
- ```json
160
- {
161
- "agents": {
162
- "hermes": {
163
- "enabled": true,
164
- "apiKey": "你的API_SERVER_KEY值"
165
- }
166
- }
167
- }
95
+ ```bash
96
+ linco-connect doctor
168
97
  ```
169
98
 
170
- 或设置环境变量:
99
+ 本地测试页默认不连接模拟 IM。需要本地联调时显式开启:
171
100
 
172
101
  ```bash
173
- export LINCO_HERMES_API_KEY=你的API_SERVER_KEY值
102
+ linco-connect start --local-im
174
103
  ```
175
104
 
176
- 如果连接时报 `401`,请检查 Hermes `api_server.key` 是否与 `LINCO_HERMES_API_KEY` / `agents.hermes.apiKey` 一致。自动启动会保留用户已有 key;如果 key 缺失,会生成新的本机 API key,当前和后续连接器进程会从 Hermes 配置中读取并使用。
177
-
178
- ## 配置文件
105
+ 启动后终端会输出带访问 token 的本地测试页地址。不要把该地址或 token 发给不可信的人。
179
106
 
180
- 配置统一保存在用户主目录下的 `.linco` 根目录:
107
+ ## 配置
181
108
 
182
- ```text
183
- ~/.linco/config.json
184
- ```
109
+ 配置默认保存在用户目录下的 `.linco/config.json`。初始化命令会自动写入凭证和本地测试 token,一般不需要手动编辑。
185
110
 
186
- Windows 示例:
187
-
188
- ```text
189
- C:\Users\<用户名>\.linco\config.json
190
- ```
191
-
192
- 示例结构:
111
+ 最小配置结构示例:
193
112
 
194
113
  ```json
195
114
  {
196
- "localWeb": {
197
- "token": "auto-generated-local-token"
198
- },
199
115
  "defaultChannel": "linco",
200
- "defaultAccount": "default",
201
116
  "channels": {
202
117
  "linco": {
203
- "accounts": {
204
- "default": {
205
- "appId": "your-app-id",
206
- "appSecret": "your-app-secret"
118
+ "agents": {
119
+ "claude": {
120
+ "defaultAccount": "default",
121
+ "accounts": {
122
+ "default": {
123
+ "appId": "<appId>",
124
+ "appSecret": "<appSecret>",
125
+ "enabled": true
126
+ }
127
+ }
207
128
  }
208
129
  }
209
130
  }
@@ -211,341 +132,71 @@ C:\Users\<用户名>\.linco\config.json
211
132
  }
212
133
  ```
213
134
 
214
- 配置含义:
215
-
216
- - `localWeb.token`:本地测试页访问 token,首次启动或初始化时自动生成。
217
- - `defaultAccount`:默认账号名。
218
- - `appId/appSecret`:本机保存的 Linco 应用凭证;插件连接 Linco IM 时会拼成 query 参数 `token=appId:appSecret`。
219
- - `agents.claude` / `agents.codex`:按 Agent 配置启用状态、CLI 命令和可选私有化 `wsUrl` 覆盖。
220
-
221
- 默认不需要把内置 `wsUrl` 写入配置文件。启用 Codex 的最小配置示例:
222
-
223
- ```json
224
- {
225
- "agents": {
226
- "codex": {
227
- "enabled": true,
228
- "bin": "codex"
229
- }
230
- }
231
- }
232
- ```
233
-
234
- 私有化部署可覆盖某个 Agent 的连接地址:
235
-
236
- ```json
237
- {
238
- "agents": {
239
- "codex": {
240
- "enabled": true,
241
- "bin": "codex",
242
- "wsUrl": "wss://example.internal/socket/ai/codex"
243
- }
244
- }
245
- }
246
- ```
247
-
248
- ## 启动
135
+ 常用环境变量:
249
136
 
250
- 默认启动本地服务,并按配置连接远端 IM;本地测试页不会自动连接模拟前端 IM:
251
-
252
- ```bash
253
- linco-connect start
254
- ```
255
-
256
- 如果 Linco Connect 已经在运行,再次执行 `linco-connect start` `npm start` 会先停止旧进程,再启动新的进程,相当于一次 restart。
257
-
258
- 需要使用本地测试页模拟前端 IM 时,显式启用:
259
-
260
- ```bash
261
- linco-connect start --local-im
262
- # 等价写法
263
- linco-connect start --mock-im
264
- ```
265
-
266
- 后台启动:
267
-
268
- ```bash
269
- linco-connect start --daemon
270
- ```
271
-
272
- 如果后台服务已经在运行,再次执行 `linco-connect start --daemon` 也会先停止旧进程,再启动新的后台进程。
273
-
274
- 后台启动并启用本地模拟前端 IM:
275
-
276
- ```bash
277
- linco-connect start --daemon --local-im
278
- ```
279
-
280
- 停止后台服务:
281
-
282
- ```bash
283
- linco-connect stop
284
- ```
285
-
286
- 后台模式会把 PID 和日志写入:
287
-
288
- ```text
289
- ~/.linco/linco-connect.pid
290
- ~/.linco/logs/daemon.out.log
291
- ~/.linco/logs/daemon.err.log
292
- ```
293
-
294
- 启动后终端会输出本地测试页地址,例如:
295
-
296
- ```text
297
- 本地测试页: http://127.0.0.1:3000/?localToken=xxxx
298
- ```
299
-
300
- 请使用这条带 `localToken` 的地址打开测试页面。直接访问 `http://127.0.0.1:3000` 会被拒绝。
301
-
302
- 源码开发时也可以继续使用:
303
-
304
- ```bash
305
- npm start
306
- ```
307
-
308
- 源码开发后台运行:
309
-
310
- ```bash
311
- npm start -- --daemon
312
- ```
313
-
314
- ## 环境检查
315
-
316
- ```bash
317
- linco-connect doctor
318
- ```
319
-
320
- 会检查:
321
-
322
- - Node.js 版本
323
- - 配置文件是否存在
324
- - `appId/appSecret` 是否已配置为 Linco token 来源
325
- - 本地测试 token 是否已生成
326
- - 已启用 Agent 的 CLI 是否可用
327
- - Windows Git Bash 是否可用
328
- - `Linco Home`、sessions 目录是否可写
329
-
330
- `doctor` 不会请求真实 IM 服务端做鉴权;`token=appId:appSecret` 的鉴权由 Linco IM 服务端负责。
331
-
332
- ## 本地自测页面
333
-
334
- 本地页面仅用于开发和自测:
335
-
336
- - 页面通过本机 WebSocket 连接当前插件服务。
337
- - 页面不会直接连接供应商 IM WebSocket。
338
- - 页面需要 `localToken` 才能访问。
339
- - 页面可测试消息、附件、工具调用、权限确认和 outbox 文件下发。
340
-
341
- ## 附件上传
137
+ | 变量 | 说明 |
138
+ | --- | --- |
139
+ | `LINCO_TOKEN` | `<appId>:<appSecret>` 简写形式 |
140
+ | `LINCO_AGENT` | 当前默认 Agent 类型 |
141
+ | `LINCO_ACCOUNT` | 当前账号名 |
142
+ | `LINCO_HOME` | 运行数据目录 |
143
+ | `LINCO_LOCAL_AGENT` | 本地测试页默认 Agent |
144
+ | `LINCO_CLAUDE_ENABLED` | 是否启用 Claude |
145
+ | `LINCO_CODEX_ENABLED` | 是否启用 Codex |
146
+ | `LINCO_HERMES_ENABLED` | 是否启用 Hermes |
147
+ | `LINCO_OPENCLAW_ENABLED` | 是否启用 OpenClaw |
148
+ | `LINCO_<AGENT>_BIN` | 覆盖对应 Agent CLI 命令或路径 |
149
+ | `LINCO_<AGENT>_WS_URL` | 私有化部署时覆盖对应 Agent 的连接地址 |
150
+
151
+ ## 附件和文件下发
152
+
153
+ 默认支持常见图片、文本、表格、文档、PDF、压缩包等附件。高风险可执行文件和脚本扩展名默认会被拦截。
342
154
 
343
155
  默认限制:
344
156
 
345
- - 单次最多 50 个附件
346
- - 单文件最大 50 MB
347
- - 单次附件总大小最大 250 MB
348
- - 默认拦截可执行文件和高风险脚本扩展名,例如 `.exe`、`.bat`、`.cmd`、`.ps1`、`.dll`
349
-
350
- 图片附件会直接作为当前 Agent 的多模态图片输入;Codex 当前会降级为文本占位提示。普通文件会保存到当前 session 的附件目录,并把本地路径附加给 Agent。
351
-
352
- ## Agent 生成文件下发
353
-
354
- 每个 session 都有独立 outbox 目录。Agent 如果需要把文件发给用户,应保存或复制到该目录。服务端会自动扫描新文件并推送到前端显示。
355
-
356
- 可通过本地命令查看路径:
357
-
358
- ```text
359
- /base
360
- ```
361
-
362
- 输出中会包含:
157
+ | 项目 | 限制 |
158
+ | --- | --- |
159
+ | 单次附件数量 | 50 |
160
+ | 单文件大小 | 50 MB |
161
+ | 单次附件总大小 | 250 MB |
162
+ | outbox 单文件大小 | 50 MB |
363
163
 
364
- ```text
365
- 附件目录: .../attachments
366
- outbox 目录: .../outbox
367
- ```
164
+ Agent 需要把文件发给用户时,可先执行 `/base` 查看当前会话目录,然后把文件保存或复制到 outbox 目录。连接器会自动扫描新文件并推送到前端。
368
165
 
369
166
  ## 斜杠命令
370
167
 
371
168
  | 命令 | 说明 |
372
169
  | --- | --- |
373
- | `/help` | 显示可用命令和附件说明 |
374
- | `/commands` | 显示本地命令和当前 Agent 原生命令说明 |
170
+ | `/help` | 显示快速帮助 |
171
+ | `/commands` | 显示本地命令和 Agent 原生命令说明 |
375
172
  | `/status` | 显示当前会话状态 |
376
173
  | `/pwd` | 显示当前工作目录 |
377
174
  | `/cd` | 列出当前目录内容 |
378
- | `/cd <路径>` | 切换工作目录,并开启新的 Agent 会话 |
175
+ | `/cd <路径>` | 切换工作目录并开启新 Agent 会话 |
379
176
  | `/new` | 开启新 Agent 会话,清除上下文 |
380
- | `/stop` | 停止当前 Agent 进程,下次消息尝试恢复当前会话 |
381
- | `/base` | 显示 Linco 运行目录、附件目录和 outbox 目录 |
382
- | `/list` | 列出当前 IM 会话最近 10 条 Agent Session 历史 |
383
- | `/list <条数>` | 按指定条数列出最近的 Agent Session 历史 |
177
+ | `/stop` | 停止当前 Agent 进程,保留可恢复会话 ID |
178
+ | `/base` | 显示运行目录、附件目录和 outbox 目录 |
179
+ | `/list [条数]` | 列出当前 IM 会话下最近的 Agent Session 历史 |
180
+ | `/switch <序号或ID>` | 切换到指定 Agent Session,恢复上下文 |
181
+ | `/delete <序号或ID>` | 从历史记录中删除指定 Agent Session |
384
182
  | `/approve` | 显示当前自动审批状态 |
385
- | `/approve on` | 开启当前会话自动审批,后续工具/命令权限请求和危险操作确认自动允许(默认) |
386
- | `/approve off` | 关闭当前会话自动审批,恢复手动确认 |
387
-
388
- 除上述本地命令外,其他 `/xxx` 会透传给当前 Agent CLI。
389
-
390
- ## 常用环境变量
391
-
392
- | 变量 | 默认值 | 说明 |
393
- | --- | --- | --- |
394
- | `PORT` | `3000` | 本地 HTTP 服务端口 |
395
- | `HOST` | `127.0.0.1` | 本地 HTTP 服务监听地址 |
396
- | `CLAUDE_BIN` | `claude` | Claude Code CLI 命令或路径 |
397
- | `CODEX_BIN` | `codex` | Codex CLI 命令或路径 |
398
- | `LINCO_CLAUDE_ENABLED` | 无配置文件时为启用 | 是否启用 Claude 连接器 |
399
- | `LINCO_CODEX_ENABLED` | `false` | 是否启用 Codex 连接器 |
400
- | `LINCO_HERMES_ENABLED` | `false` | 是否启用 Hermes 连接器 |
401
- | `LINCO_HERMES_GATEWAY_URL` | `http://127.0.0.1:8642` | Hermes Gateway 地址 |
402
- | `LINCO_HERMES_AUTO_START_GATEWAY` | `true` | Gateway 未运行时是否自动补齐配置并启动 |
403
- | `LINCO_HERMES_BIN` / `HERMES_BIN` | `hermes` | Hermes CLI 命令或路径 |
404
- | `LINCO_HERMES_PROFILE` | `default` | Hermes profile 名称 |
405
- | `LINCO_HERMES_HOME` / `HERMES_HOME` | 自动检测 | Hermes home 目录 |
406
- | `LINCO_HERMES_API_KEY` | 无 | Hermes Gateway 接口鉴权 Key |
407
- | `LINCO_CLAUDE_WS_URL` | `wss://chat.ddjf.info/socket/ai/claude` | Claude 端点覆盖 |
408
- | `LINCO_CODEX_WS_URL` | `wss://chat.ddjf.info/socket/ai/codex` | Codex 端点覆盖 |
409
- | `LINCO_LOCAL_AGENT` | `claude` | 本地测试页默认使用的 Agent |
410
- | `CLAUDE_SYSTEM_PROMPT` | 内置中文 Markdown 输出提示词 | 附加给 Claude 的系统提示词 |
411
- | `CLAUDE_CODE_GIT_BASH_PATH` | 自动检测 | Windows 下 Git Bash 路径 |
412
- | `MAX_WS_PAYLOAD_BYTES` | `367001600` | WebSocket 最大消息体大小 |
413
- | `MAX_ATTACHMENT_COUNT` | `50` | 单次最大附件数量 |
414
- | `MAX_ATTACHMENT_BYTES` | `52428800` | 单附件大小限制 |
415
- | `MAX_TOTAL_ATTACHMENT_BYTES` | `262144000` | 单次附件总大小限制 |
416
- | `MAX_OUTGOING_ATTACHMENT_BYTES` | `52428800` | outbox 单文件下发大小限制 |
417
- | `MAX_MESSAGE_QUEUE` | `10` | Claude 忙碌时的最大排队消息数 |
418
- | `ATTACHMENTS_DIR_NAME` | `attachments` | session 运行目录下的附件目录名 |
419
- | `OUTBOX_DIR_NAME` | `outbox` | session 运行目录下的下发目录名 |
420
- | `ALLOW_UNSAFE_ATTACHMENTS` | `0` | 设置为 `1` 时允许上传默认拦截的高风险扩展名 |
421
- | `UNSAFE_ATTACHMENT_EXTENSIONS` | 内置扩展名列表 | 逗号分隔的高风险扩展名列表 |
422
- | `LINCO_CHANNEL` | `linco` | 选择配置文件中的 IM 渠道 |
423
- | `LINCO_ACCOUNT` | `default` | 选择配置文件中的 IM 账号 |
424
- | `LINCO_TOKEN` | 空 | `appId:appSecret` 简写形式;连接 Linco 时作为 query 参数 `token` 发送 |
425
- | `LINCO_APP_ID` | 配置文件值 | Linco IM 应用 ID |
426
- | `LINCO_APP_SECRET` | 配置文件值 | Linco IM 应用密钥 |
427
- | `LINCO_AGENT_ID` | `main` | Linco 连接器标识,可由配置文件或环境变量指定 |
428
- | `LINCO_WS_URL` | `wss://chat.ddjf.info/socket/ai/claude` | Claude 旧版端点覆盖,普通用户不需要配置 |
429
- | `LINCO_HOME` | `~/.linco` | 运行数据根目录 |
430
- | `LINCO_SESSIONS_DIR` | `~/.linco/sessions` | session 数据目录 |
431
- | `CLAUDE_GRACEFUL_SHUTDOWN_MS` | `3000` | Claude 进程优雅关闭时间 |
432
-
433
- ## 运行目录
434
-
435
- 默认目录结构:
436
-
437
- ```text
438
- ~/.linco/
439
- ├── config.json
440
- ├── claude/
441
- │ └── sessions/
442
- │ └── sid_xxxxxxxx/
443
- │ ├── session.json
444
- │ ├── workspace/
445
- │ ├── attachments/
446
- │ └── outbox/
447
- └── codex/
448
- └── sessions/
449
- └── sid_xxxxxxxx/
450
- ├── session.json
451
- ├── workspace/
452
- ├── attachments/
453
- └── outbox/
454
- ```
183
+ | `/approve on` | 开启自动审批 |
184
+ | `/approve off` | 关闭自动审批,恢复手动确认 |
185
+ | `/usage` | 显示 Token 用量统计;部分 Agent 可能暂不提供 |
455
186
 
456
- 说明:
187
+ 除上述本地命令外,其他 `/xxx` 会透传给当前 Agent。部分 Agent 原生命令只适合交互式 CLI/TUI,在桥接模式下可能没有输出。
457
188
 
458
- - `config.json` 保存用户配置和本地测试 token。
459
- - `claude/sessions/` 和 `codex/sessions/` 分别保存对应 Agent 的会话元数据、工作目录、上传附件和下发文件。
460
- - `attachments/` 保存用户上传的普通文件。
461
- - `outbox/` 用于 Agent 生成文件并自动下发给前端。
462
- - Codex 默认使用系统安装时的认证和配置,不会创建项目托管的 `codex-home`。
189
+ ## 安全注意
463
190
 
464
- ## 故障排查
465
-
466
- ### Hermes 模式报 "fetch failed" 错误
467
-
468
- 默认情况下,`ddchat-connect` 会在第一次 Hermes 对话前自动检查并启动 Hermes Gateway。如果仍然报 **"Hermes 错误: fetch failed"**,通常是 Hermes CLI 不可用、Hermes 自身模型配置异常、端口被占用,或配置了外部 Gateway 但未运行。
469
-
470
- 先运行:
471
-
472
- ```bash
473
- linco-connect doctor
474
- ```
475
-
476
- 排查方向:
477
-
478
- - 确认终端可执行 `hermes`,或通过 `agents.hermes.hermesBin` / `LINCO_HERMES_BIN` 配置 Hermes 可执行文件路径。
479
- - 确认 Hermes 自身模型、账号或 provider 配置可用。
480
- - 确认 `gatewayUrl` 对应端口未被其他程序占用。
481
- - 如果使用 hermes-web-ui 已启动的 Gateway,确认 `gatewayUrl` 和 `profile` 与 web-ui 当前 Gateway 一致。
482
- - 如果返回 `401`,确认 `agents.hermes.apiKey` / `LINCO_HERMES_API_KEY` 与 Hermes `api_server.key` 一致。
483
-
484
- 高级排障时可手动检查 Hermes profile 的 `config.yaml`,Gateway 配置应使用 `extra.host` / `extra.port`:
485
-
486
- ```yaml
487
- platforms:
488
- api_server:
489
- enabled: true
490
- key: '<本机 API key>'
491
- cors_origins: 'http://127.0.0.1:*'
492
- extra:
493
- host: 127.0.0.1
494
- port: 8642
495
- ```
496
-
497
- 确认 Gateway 健康:
498
-
499
- ```bash
500
- curl http://127.0.0.1:8642/health
501
- ```
502
-
503
-
504
- ## 安全注意事项
505
-
506
- - 默认只监听 `127.0.0.1`,不建议直接暴露到公网。
507
- - 本地测试页受 `localToken` 保护,请不要把带 token 的 URL 发给不可信的人。
508
- - `appSecret` 保存在本机 `~/.linco/config.json`,请保护好用户电脑和配置文件。
509
- - 插件不负责校验 `token=appId:appSecret`;真实鉴权由 Linco IM 服务端完成。
510
- - Agent 的工具权限请求会转发给前端确认,但最终权限边界仍取决于本机 Agent 配置和用户批准。
511
- - 危险命令检测只是基础提示,不应视为完整沙箱或安全隔离机制。
512
- - 上传附件会落盘到本机,请不要上传或下发不应保存在本机的敏感文件。
513
-
514
- ## 项目结构
515
-
516
- ```text
517
- .
518
- ├── bin/
519
- │ └── linco.js # npm CLI 入口
520
- ├── public/
521
- │ └── index.html # 本地自测页面
522
- ├── src/
523
- │ ├── agentRunner.js # Agent 运行器门面
524
- │ ├── agents/ # Claude/Codex Provider
525
- │ ├── attachmentHandler.js # 上传附件保存、校验和 Agent 输入构造
526
- │ ├── claudeRunner.js # Claude Code 子进程、stream-json 解析、权限处理
527
- │ ├── config.js # 配置文件、环境变量和 Git Bash 检测
528
- │ ├── danger.js # 高风险命令文本检测
529
- │ ├── httpStatic.js # 静态文件、本地 client config 和 outbox 下载接口
530
- │ ├── localAuth.js # 本地测试 token 生成和校验
531
- │ ├── outgoingAttachmentHandler.js # outbox 扫描和附件下发
532
- │ ├── protocol.js # WebSocket 消息封装
533
- │ ├── serverApp.js # 可复用服务启动入口
534
- │ ├── session.js # session 状态和 Agent 进程清理
535
- │ ├── slashCommands.js # /help、/cd、/new 等斜杠命令
536
- │ └── wsServer.js # WebSocket 连接、消息分发和 session 生命周期
537
- ├── server.js # 兼容 npm start 的入口
538
- ├── package.json
539
- └── package-lock.json
540
- ```
191
+ - 不要公开配置文件、访问 token、`appSecret` 或带 token 的本地测试页地址。
192
+ - 本地测试页只用于开发和自测。
193
+ - 自动审批会允许后续工具/命令权限请求和危险操作确认;如需人工确认,请在会话内执行 `/approve off`。
194
+ - 附件会保存到本机,请避免上传不应落盘的敏感文件。
541
195
 
542
196
  ## 开发命令
543
197
 
544
198
  ```bash
545
199
  npm install
546
200
  npm start
547
- node bin/linco-connect.js --help
548
201
  node bin/linco-connect.js doctor
549
202
  ```
550
-
551
- 当前项目没有配置测试、lint 或构建脚本。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linco-connect",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "自研 IM 桥接多 Agent 服务",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -54,6 +54,7 @@ function runAppServerTurn(input, ws, session, config) {
54
54
  session.sawPartialAssistantText = false;
55
55
  session.codexAssistantEnded = false;
56
56
  session.codexEmittedAgentMessageIds = new Set();
57
+ session.codexToolStates = new Map();
57
58
  resetCodexAssistantText(session);
58
59
  session._lastWs = ws;
59
60
  session._lastConfig = config;
@@ -358,6 +359,68 @@ function hasCodexAgentMessageEmitted(session, itemId) {
358
359
  return Boolean(id && session.codexEmittedAgentMessageIds?.has(id));
359
360
  }
360
361
 
362
+ function ensureCodexToolStates(session) {
363
+ if (!(session.codexToolStates instanceof Map)) {
364
+ session.codexToolStates = new Map();
365
+ }
366
+ return session.codexToolStates;
367
+ }
368
+
369
+ function codexToolStateFor(session, id) {
370
+ const toolId = String(id || '').trim();
371
+ if (!toolId) return null;
372
+ const states = ensureCodexToolStates(session);
373
+ return states.get(toolId) || null;
374
+ }
375
+
376
+ function setCodexToolState(session, id, next) {
377
+ const toolId = String(id || '').trim();
378
+ if (!toolId) return;
379
+ const states = ensureCodexToolStates(session);
380
+ const previous = states.get(toolId) || {};
381
+ states.set(toolId, {
382
+ ...previous,
383
+ ...next,
384
+ });
385
+ }
386
+
387
+ function emitCodexToolCall(ws, session, tool) {
388
+ if (!ws) return false;
389
+ const id = String(tool.id || '').trim();
390
+ const existing = codexToolStateFor(session, id);
391
+ if (existing?.phase === 'completed' || existing?.phase === 'started') {
392
+ return false;
393
+ }
394
+ const name = String(tool.name || existing?.name || 'tool').trim() || 'tool';
395
+ const input = tool.input ?? existing?.input ?? '';
396
+ send(ws, 'tool_call', { id, name, input });
397
+ setCodexToolState(session, id, { phase: 'started', name, input });
398
+ return true;
399
+ }
400
+
401
+ function emitCodexToolResult(ws, session, tool) {
402
+ if (!ws) return false;
403
+ const id = String(tool.id || '').trim();
404
+ const existing = codexToolStateFor(session, id);
405
+ if (existing?.phase === 'completed') return false;
406
+
407
+ const name = String(tool.name || existing?.name || 'tool').trim() || 'tool';
408
+ const input = tool.input ?? existing?.input ?? '';
409
+ if (id && existing?.phase !== 'started') {
410
+ emitCodexToolCall(ws, session, { id, name, input });
411
+ }
412
+
413
+ const output = tool.output ?? '';
414
+ send(ws, 'tool_result', { id, output });
415
+ setCodexToolState(session, id, {
416
+ phase: 'completed',
417
+ name,
418
+ input,
419
+ output,
420
+ });
421
+ return true;
422
+ }
423
+
361
424
  function shouldAppendCompletedAgentMessage(session, params) {
362
425
  const itemId = codexAgentMessageId(params);
363
426
  if (itemId) return !hasCodexAgentMessageEmitted(session, itemId);
@@ -514,14 +577,12 @@ function handleServerRequest(message, session) {
514
577
 
515
578
  // Tool call from server request
516
579
  if (method === 'item/tool/call') {
517
- if (ws) {
518
- const toolName = params.name || params.tool || '';
519
- send(ws, 'tool_call', {
520
- id: String(message.id),
521
- name: toolName,
522
- input: JSON.stringify(params.input || {}).slice(0, 300),
523
- });
524
- }
580
+ const toolName = params.name || params.tool || '';
581
+ emitCodexToolCall(ws, session, {
582
+ id: String(message.id),
583
+ name: toolName,
584
+ input: JSON.stringify(params.input || {}).slice(0, 300),
585
+ });
525
586
  sendJsonRpc(session.codexAppServer, {
526
587
  jsonrpc: '2.0',
527
588
  id: message.id,
@@ -605,16 +666,19 @@ function handleAppServerMessage(message, session) {
605
666
  session._log?.info('codex item completed', { itemType, item: summarizeCodexItemForLog(params.item) });
606
667
  if (itemType === 'toolCall' || itemType === 'commandExecution' || itemType === 'webSearch') {
607
668
  const itemId = params.item?.id || params.itemId || '';
608
- if (itemType === 'webSearch' && params.item?.query) {
609
- send(ws, 'tool_call', {
610
- id: itemId,
611
- name: 'webSearch',
612
- input: params.item.query,
613
- });
614
- }
669
+ const isCommand = itemType === 'commandExecution';
670
+ const isWebSearch = itemType === 'webSearch';
671
+ const toolName = isCommand ? 'exec' : isWebSearch ? 'webSearch' : (params.item?.name || params.item?.tool || '');
672
+ const toolInput = isCommand
673
+ ? (params.item?.command || '')
674
+ : isWebSearch
675
+ ? (params.item?.query || params.item?.input || params.item?.arguments || {})
676
+ : (params.item?.input || params.item?.arguments || {});
615
677
  const output = params.item?.output || params.item?.result || params.item?.results || params.output || params.result || '';
616
- send(ws, 'tool_result', {
678
+ emitCodexToolResult(ws, session, {
617
679
  id: itemId,
680
+ name: toolName,
681
+ input: typeof toolInput === 'string' ? toolInput : JSON.stringify(toolInput).slice(0, 300),
618
682
  output: typeof output === 'string' ? output : JSON.stringify(output).slice(0, 1000),
619
683
  });
620
684
  return;
@@ -652,7 +716,7 @@ function handleAppServerMessage(message, session) {
652
716
  if (method === 'tool/start' || method === 'tool_call') {
653
717
  const toolName = params.name || params.tool || '';
654
718
  if (toolName) {
655
- send(ws, 'tool_call', {
719
+ emitCodexToolCall(ws, session, {
656
720
  id: params.id || params.toolId || '',
657
721
  name: toolName,
658
722
  input: params.input || params.arguments || {},
@@ -662,7 +726,7 @@ function handleAppServerMessage(message, session) {
662
726
  }
663
727
 
664
728
  if (method === 'tool/completed' || method === 'tool_result') {
665
- send(ws, 'tool_result', {
729
+ emitCodexToolResult(ws, session, {
666
730
  id: params.id || params.toolId || '',
667
731
  output: params.output || params.result || '',
668
732
  });
@@ -683,7 +747,7 @@ function handleAppServerMessage(message, session) {
683
747
  : (params.item?.input || params.item?.arguments || {});
684
748
  const itemId = params.item?.id || params.itemId || '';
685
749
  if (toolName) {
686
- send(ws, 'tool_call', {
750
+ emitCodexToolCall(ws, session, {
687
751
  id: itemId,
688
752
  name: toolName,
689
753
  input: typeof toolInput === 'string' ? toolInput : JSON.stringify(toolInput).slice(0, 300),