@licity/qclaw-local-connector 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -1,14 +1,83 @@
1
+ # ──────────────────────────────────────────────────────────────────────────────
2
+ # 里世界 OpenClaw 本地连接器 · .env 配置文件
3
+ # 用任意文本编辑器(如记事本)打开,按说明填写后保存,重新运行 licity-connector。
4
+ # 带【勿改】标记的字段直接保留默认值;带【必填】的必须根据你的电脑情况修改。
5
+ # ──────────────────────────────────────────────────────────────────────────────
6
+
7
+ # 【勿改】里世界服务器地址,固定不动。
1
8
  LICITY_API_BASE_URL=https://li.city
2
- OPENCLAW_CONNECTOR_SECRET=replace-with-your-connector-secret
3
- CONNECTOR_PROVIDER=qclaw-local
9
+
10
+ # 【可选】旧版连接器验证密钥。
11
+ # 新版推荐直接留空:首次运行会先显示二维码,扫码通过后自动下发并保存本机连接令牌。
12
+ # 只有你要兼容旧版脚本或手动调用受保护接口时,才需要填写这个全局密钥。
13
+ OPENCLAW_CONNECTOR_SECRET=
14
+
15
+ # 【勿改】连接器类型标识,告诉里世界服务器这是 OpenClaw 本地连接器,固定不动。
16
+ CONNECTOR_PROVIDER=openclaw-local
17
+
18
+ # 【可选】这台电脑的别名,多台电脑接入时方便区分。
19
+ # 留空时自动使用电脑的主机名(hostname)。可以填中文,例如:我的台式机
4
20
  DEVICE_NAME=
21
+
22
+ # ──────────────────────────────────────────────────────────────────────────────
23
+ # Runtime 路径相关(以下字段优先兼容 OpenClaw,也兼容 QClaw)
24
+ # ──────────────────────────────────────────────────────────────────────────────
25
+
26
+ # 【可选】宿主 Runtime 主程序(.exe)完整路径。
27
+ # 如果你使用 QClaw,可直接填 QClaw 主程序路径;其他宿主可填其实际启动文件。
28
+ OPENCLAW_RUNTIME_PATH=D:\QClaw\QClaw.exe
29
+
30
+ # 兼容旧版字段。若你已经在用 QCLAW_PATH,可继续保留。
5
31
  QCLAW_PATH=D:\QClaw\QClaw.exe
32
+
33
+ # 【必填】OpenClaw / 宿主 Runtime 的状态目录(存放配置文件、登录状态等)。
34
+ # ⚠️ 必须把 Administrator 改成你电脑的 Windows 用户名(中文名也可以)!
35
+ # 怎么查用户名:按 Win+R → 输入 cmd 回车 → 输入 echo %USERNAME% → 回车查看。
36
+ # 确认方法:打开文件管理器 → 地址栏输入此路径 → 能看到文件夹说明路径正确。
37
+ OPENCLAW_STATE_DIR=C:\Users\Administrator\.qclaw
38
+
39
+ # 兼容旧版字段。
6
40
  QCLAW_STATE_DIR=C:\Users\Administrator\.qclaw
41
+
42
+ # 【可选】宿主 Runtime 主配置文件路径。
43
+ # QClaw 用户可继续填写 qclaw.json,其他宿主没有这个文件也不影响扫码连接。
7
44
  QCLAW_CONFIG_PATH=C:\Users\Administrator\.qclaw\qclaw.json
45
+
46
+ # 【必填】OpenClaw(QClaw 内置 AI 框架)的配置文件路径。
47
+ # 值 = QCLAW_STATE_DIR 后面加 \openclaw.json,把 Administrator 改成你的用户名即可。
48
+ # 这个文件由 QClaw 自动维护,正常情况下只读不写。
8
49
  OPENCLAW_CONFIG_PATH=C:\Users\Administrator\.qclaw\openclaw.json
50
+
51
+ # 【必填】OpenClaw 命令行程序(.mjs)的完整路径。
52
+ # 连接器通过这个程序直接调用 QClaw 的 AI 能力来执行任务。
53
+ # 路径格式固定:「QClaw 安装目录」\resources\openclaw\node_modules\openclaw\openclaw.mjs
54
+ # 把开头的 D:\QClaw 改成你的 QClaw 实际安装目录,后面的路径不变。
9
55
  OPENCLAW_CLI_PATH=D:\QClaw\resources\openclaw\node_modules\openclaw\openclaw.mjs
56
+
57
+ # ──────────────────────────────────────────────────────────────────────────────
58
+ # 以下为可选配置项,默认值通常够用,不需要改动
59
+ # ──────────────────────────────────────────────────────────────────────────────
60
+
61
+ # 【勿改】使用 OpenClaw 的哪个 Agent 执行任务,通常固定填 main。
10
62
  OPENCLAW_AGENT_ID=main
63
+
64
+ # 【可选】如果你不是通过 QClaw 网关提供模型能力,也可以直接指定 OpenClaw 网关地址与鉴权。
65
+ OPENCLAW_GATEWAY_BASE_URL=
66
+ OPENCLAW_GATEWAY_API_KEY=
67
+ OPENCLAW_WECHAT_WS_URL=
68
+
69
+ # 【可选】AI 执行一次任务的最长等待时间(毫秒)。
70
+ # 默认 60000 = 60 秒。如果 AI 经常超时,可以调大,例如 120000(2 分钟)。
11
71
  OPENCLAW_COMMAND_TIMEOUT_MS=60000
72
+
73
+ # 【勿改】此连接器支持的功能域,不要修改。
74
+ # private_chat=私信回复, neighbor=邻里圈, anchor=穿越锚点, time_travel=穿越
12
75
  CAPABILITY_SCOPES=private_chat,neighbor,anchor,time_travel
76
+
77
+ # 【可选】连接器向里世界服务器发送心跳信号的间隔(毫秒)。
78
+ # 默认 25000 = 25 秒。心跳用于维持龙虾「在线」状态,超过 3 分钟无心跳会标记离线。
13
79
  HEARTBEAT_INTERVAL_MS=25000
80
+
81
+ # 【可选】连接器轮询是否有新任务(私信等)的频率(毫秒)。
82
+ # 默认 3000 = 3 秒查询一次。值越小响应越快,但服务器请求也越多。
14
83
  POLL_INTERVAL_MS=3000
package/README.md CHANGED
@@ -1,491 +1,204 @@
1
- # 里世界 QClaw 本地连接器
1
+ # 里世界 OpenClaw 本地连接器
2
2
 
3
- 这是把本机 QClaw 或其他第三方本地 Runtime,桥接到里世界 APP 龙虾系统的本地连接器。
3
+ 这是里世界的本地龙虾连接器。它负责把你电脑上的 OpenClaw Runtime 接进里世界 APP,让某只龙虾真正映射到本机运行。
4
4
 
5
- 它解决的不是“让 QClaw 自己联网找里世界”,而是另一件事:
5
+ 它已经不再要求你先手填全局密钥。现在的流程是:
6
6
 
7
- - 在你电脑上生成一个给里世界 APP 扫描的连接二维码。
8
- - 扫码成功后,把某只龙虾和你这台电脑上的 Runtime 绑定起来。
9
- - 后续由里世界把任务派发给本地连接器,再由本地连接器转给 QClaw/OpenClaw 执行。
7
+ 1. 在电脑上运行一条命令。
8
+ 2. 终端立即出现二维码。
9
+ 3. 用里世界 APP 扫码并选择龙虾确认。
10
+ 4. 连接器自动拿到本机专用令牌并保存到本地。
10
11
 
11
- 如果你是第一次接触这个链路,先记住一句话:
12
+ 当前包名仍然保留为 @licity/qclaw-local-connector,是为了兼容旧用户;但从能力上,它已经是通用 OpenClaw 连接器,不再只限 QClaw。
12
13
 
13
- 只启动本目录里的连接器还不够。QClaw 主程序、本地 19000 网关、连接器扫码绑定,这三层必须同时成立。
14
+ ## 适用范围
14
15
 
15
- ## 安装方式
16
+ 适合以下场景:
16
17
 
17
- ### 方式一:npm 全局安装(推荐)
18
+ - 你使用 QClaw,且本机带有 OpenClaw CLI。
19
+ - 你使用其他兼容 OpenClaw CLI 的本地 Runtime。
20
+ - 你希望把本机 AI 能力映射成里世界里的龙虾,由 APP 给龙虾发消息,本机 Runtime 回复。
18
21
 
19
- **第一步:安装 Node.js**
22
+ 不适合以下场景:
20
23
 
21
- 没有 Node.js 的需要先安装它(npm 命令附带在 Node.js 里)。
24
+ - 你只是要做 MCP 直连工具调用,不需要把本机 Runtime 映射成龙虾。
25
+ - 你手里的官方 Runtime 已经内置里世界原生扫码绑定,并不需要桥接器。
22
26
 
23
- 1. 打开浏览器,访问 [nodejs.org](https://nodejs.org)
24
- 2. 点击显示"LTS"的大按钮下载安装包(Windows 下载 `.msi` 文件)
25
- 3. 下载完成后双击运行,一直点"Next"直到安装完成,保持所有默认选项
26
- 4. 安装完后打开终端(见下一步),输入 `node -v`,能看到版本号说明安装成功
27
+ ## 一条命令启动
27
28
 
28
- **如何打开终端:**
29
- - **Windows 11/10**:按 `Win + X`,选择"终端"或"PowerShell";或按 `Win + R`,输入 `cmd` 回车
30
- - **macOS**:按 `Cmd + 空格`,搜索"Terminal"打开
31
- - **或者**:在文件夹空白处右键,选择"在此处打开终端"/"在终端中打开"
29
+ 无需全局安装时,可以直接运行:
32
30
 
33
- **第二步:全局安装连接器**
31
+ ```bash
32
+ npx @licity/qclaw-local-connector
33
+ ```
34
34
 
35
- 在终端里输入以下命令并回车:
35
+ 如果你希望长期使用,也可以全局安装:
36
36
 
37
37
  ```bash
38
38
  npm install -g @licity/qclaw-local-connector
39
39
  ```
40
40
 
41
- 等待安装完成,看到 `added X packages` 字样即成功。安装过程中出现 `WARN` 字样是正常的,不影响使用。
42
-
43
- > **⚠️ Windows PowerShell 提示"禁止运行脚本"怎么办?**
44
- >
45
- > 如果出现以下错误:
46
- > ```
47
- > npm : 无法加载文件 ...npm.ps1,因为在此系统上禁止运行脚本
48
- > ```
49
- > 原因是 Windows PowerShell 默认禁止运行第三方脚本。有两个解决方案:
50
- >
51
- > **方案 A(推荐):改用 cmd 而不是 PowerShell**
52
- > - 按 `Win + R`,输入 `cmd` 回车,打开命令提示符(不是 PowerShell)
53
- > - 在 cmd 里重新运行 `npm install -g @licity/qclaw-local-connector`
54
- >
55
- > **方案 B:修改 PowerShell 执行策略(需要管理员权限)**
56
- > - 右键点击开始菜单 → 选择"Windows PowerShell(管理员)"或"终端(管理员)"
57
- > - 在管理员 PowerShell 里输入:
58
- > ```powershell
59
- > Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
60
- > ```
61
- > - 按 `Y` 确认,然后重新运行安装命令
62
-
63
- **第三步:创建工作目录**
64
-
65
- 在电脑上选一个记得住的文件夹作为工作目录。例如在桌面新建一个 `licity` 文件夹。
66
-
67
- > ⚠️ **重要**:不要在 npm 全局安装目录里直接运行(路径包含 `node_modules/@licity`)。请始终在你自己建的工作目录里运行。
68
-
69
- **第四步:初次运行,自动生成 `.env` 配置文件**
70
-
71
- 在工作目录(例如桌面的 `licity` 文件夹)里右键 → 在此处打开终端,然后运行:
41
+ 安装后可使用以下任一命令:
72
42
 
73
43
  ```bash
74
44
  licity-connector
75
45
  ```
76
46
 
77
- **如果是第一次运行且当前目录没有 `.env` 文件,连接器会自动在当前目录生成一个 `.env` 模板文件,然后退出并提示你填写。**
78
-
79
- 用文本编辑器打开自动生成的 `.env` 文件,按第 4 步说明填写必填项,保存后重新运行 `licity-connector` 即可。
80
-
81
- 不需要手动创建 `.env` 文件 — 第一次运行会帮你生成。
82
-
83
- ### 方式二:本地克隆 / 下载后运行
84
-
85
- 进入连接器目录,执行:
86
-
87
47
  ```bash
88
- npm install && npm run quickstart
48
+ licity-openclaw-connector
89
49
  ```
90
50
 
91
- ## 一图看懂
92
-
93
- 1. 先启动 QClaw 主程序。
94
- 2. 确认 127.0.0.1:19000 已监听。
95
- 3. 创建 `.env` 配置文件并填写必要字段(见第 4 步)。
96
- 4. 运行连接器(`licity-connector` 或 `npm run quickstart`)。
97
- 5. 终端出现二维码。
98
- 6. 打开里世界 APP 的"我的龙虾",点"扫码连接本地龙虾"。
99
- 7. 选择一只龙虾并确认连接。
100
- 8. 到龙虾私聊页测试文本,再测试截图或文件能力。
101
-
102
- ## 这套方案现在已经支持什么
103
-
104
- 当前已经打通:
105
-
106
- - 文本私聊回复。
107
- - 截图类任务回传图片。
108
- - 第三方 Runtime 按结构化附件格式回传图片或文件。
109
- - 心跳保活、扫码绑定、换账号接管 Runtime。
110
-
111
- 当前仍有限制:
112
-
113
- - 普通问答是否能真正执行,仍取决于你本机 QClaw/OpenClaw 环境是否可用。
114
- - 连接器默认通过 JSON 回传附件,建议单个附件控制在 8MB 以内。
115
- - 如果第三方程序只会回纯文本,不会输出结构化附件,那它就只能回文本,不能自动把本地文件带回里世界。
116
-
117
- ## 适合谁用
118
-
119
- 适合以下场景:
120
-
121
- - 你已经在电脑上用 QClaw。
122
- - 你希望把本机 AI 能力映射成里世界里的龙虾。
123
- - 你想让龙虾能回复文本,或者进一步回传截图、图片、文件。
124
-
125
- 不适合以下场景:
126
-
127
- - 你手里已经是官方 OpenClaw Runtime,并且它本身就能直接生成里世界配对二维码。
128
- 这种情况优先走官方 OpenClaw 原生扫码,不需要这个桥接器。
129
-
130
- ## 先说结论
51
+ ## 零密钥扫码流程
131
52
 
132
- 要让龙虾真正工作,至少要同时满足这 3 个条件:
53
+ 第一次运行时:
133
54
 
134
- 1. QClaw 主程序已经启动,并保持登录和可用。
135
- 2. QClaw 启动后,本机本地网关已经监听,当前实际检查口径是 127.0.0.1:19000。
136
- 3. 本连接器已经启动,并成功和里世界中的某只龙虾完成扫码绑定。
55
+ - 当前目录没有 .env,程序会自动生成一份可选模板。
56
+ - 程序不会因为未填写 OPENCLAW_CONNECTOR_SECRET 而退出。
57
+ - 程序会直接创建扫码会话并显示二维码。
137
58
 
138
- 如果第 1 或第 2 条不满足,APP 里给龙虾发消息后,任务虽然会进入队列,但 OpenClaw agent 无法真实执行。
59
+ 扫码成功后:
139
60
 
140
- ## 目录里有哪些文件
61
+ - 服务端会向这台电脑下发专用 connector token。
62
+ - token 会自动写入当前工作目录下的 .licity-connector/runtime.json。
63
+ - 后续心跳、拉任务、结果回写都会自动使用这个 token。
141
64
 
142
- - `index.js`:连接器主程序。
143
- - `.env.example`:环境变量模板。
144
- - `setup.js`:本地安装助手和自检脚本。
145
- - `data/runtime.json`:保存本机 Runtime ID。
146
- - `data/screenshots/`:截图任务产生的临时图片。
65
+ 这意味着你不再需要从 APP 手动复制连接密钥,也不需要手动把密钥填进 .env。
147
66
 
148
- ## 新手最短路径
67
+ ## 最短使用步骤
149
68
 
150
- ### 1 步:启动 QClaw 主程序
69
+ ### 1. 先确认本机 Runtime 可用
151
70
 
152
- QClaw 的位置取决于你的安装路径,常见的在:
153
- - Windows:`C:\QClaw\QClaw.exe` 或安装时指定的目录
154
- - 也可以通过桌面快捷方式直接启动
71
+ 至少要满足:
155
72
 
156
- 启动后不要立刻关闭,保持 QClaw 正常运行。
73
+ - OpenClaw 配置文件存在。
74
+ - OpenClaw CLI 可执行。
75
+ - 你的宿主 Runtime 已启动,并且本地模型/网关可访问。
157
76
 
158
- ### 2 步:确认本地 19000 端口已监听
77
+ 如果你是 QClaw 用户,通常就是先启动 QClaw,并确认它的 OpenClaw 服务已经起来。
159
78
 
160
- PowerShell 执行:
79
+ ### 2. 在工作目录运行连接器
161
80
 
162
- ```powershell
163
- Get-NetTCPConnection -LocalPort 19000 -State Listen
164
- ```
165
-
166
- 如果能看到监听结果,说明 QClaw 本地网关已经起来。
167
-
168
- 如果没有结果,优先检查:
169
-
170
- - QClaw 是否真的已经启动。
171
- - QClaw 是否卡在登录页、权限弹窗、升级弹窗。
172
- - 刚启动时是否还没完全拉起网关,等 5 到 15 秒再查一次。
173
-
174
- ### 第 3 步:启动连接器
175
-
176
- **npm 全局安装用户:**
177
-
178
- 在已创建好 `.env` 的目录下直接执行:
81
+ 示例:
179
82
 
180
83
  ```bash
181
- licity-connector
84
+ npx @licity/qclaw-local-connector
182
85
  ```
183
86
 
184
- **本地克隆 / 下载用户:**
185
-
186
- 进入连接器目录后执行:
87
+ 或者:
187
88
 
188
89
  ```bash
189
- npm install && npm run quickstart
90
+ licity-openclaw-connector
190
91
  ```
191
92
 
192
- 这行命令会做两件事:
193
-
194
- 1. 安装依赖。
195
- 2. 运行安装助手,检查 `.env`、QClaw 路径、OpenClaw 配置,再启动连接器。
196
-
197
- ### 第 4 步:第一次启动时补全 `.env`
198
-
199
- **方式一(npm 全局安装)用户:**
200
-
201
- 不需要手动创建 `.env` 文件。第一次在工作目录运行 `licity-connector` 时,如果目录里没有 `.env`,程序会**自动生成**一份模板并退出,提示你填写。
202
-
203
- 然后用记事本或任何文本编辑器打开生成的 `.env` 文件,按下面说明填写必填项,保存后重新运行 `licity-connector`。
204
-
205
- **方式二(本地克隆/下载)用户:**
206
-
207
- 首次运行 `npm run quickstart` 时,安装助手会自动从 `.env.example` 生成一份 `.env` 模板,直接编辑那个文件即可。
208
-
209
- ---
210
-
211
- `.env` 文件的完整示例如下。复制粘贴后,按 **【必填/可选/勿改】** 说明逐行确认:
212
-
213
- ```env
214
- # 里世界服务器地址,勿改
215
- LICITY_API_BASE_URL=https://li.city
216
-
217
- # 【必填】连接器验证密钥
218
- # 获取方式:打开里世界 APP → 我的龙虾 → 滑到底部 → 点击"复制连接密钥"按钮
219
- # 将复制的内容粘贴到等号后面(替换掉 replace-with-your-connector-secret)
220
- OPENCLAW_CONNECTOR_SECRET=replace-with-your-connector-secret
221
-
222
- # 连接器类型,勿改
223
- CONNECTOR_PROVIDER=qclaw-local
224
-
225
- # 【必填】QClaw 主程序路径(按你的实际安装位置填写)
226
- # Windows 示例:
227
- QCLAW_PATH=C:\QClaw\QClaw.exe
228
- QCLAW_STATE_DIR=C:\Users\你的用户名\.qclaw
229
- OPENCLAW_CONFIG_PATH=C:\Users\你的用户名\.qclaw\openclaw.json
230
- OPENCLAW_CLI_PATH=C:\QClaw\resources\openclaw\node_modules\openclaw\openclaw.mjs
231
-
232
- # macOS / Linux 示例(把上面 Windows 路径注释掉,取消下面几行的注释):
233
- # QCLAW_PATH=/Applications/QClaw.app/Contents/MacOS/QClaw
234
- # QCLAW_STATE_DIR=~/.qclaw
235
- # OPENCLAW_CONFIG_PATH=~/.qclaw/openclaw.json
236
- # OPENCLAW_CLI_PATH=~/.nvm/versions/node/v20/lib/node_modules/openclaw/openclaw.mjs
237
-
238
- # 以下为可选项,默认值已经够用,一般不需要改
239
- OPENCLAW_AGENT_ID=main
240
- OPENCLAW_COMMAND_TIMEOUT_MS=60000
241
- CAPABILITY_SCOPES=private_chat,neighbor,anchor,time_travel
242
- HEARTBEAT_INTERVAL_MS=25000
243
- POLL_INTERVAL_MS=3000
244
- ```
245
-
246
- **最关键的两项**:
247
-
248
- | 字段 | 从哪里获取 |
249
- |------|-----------|
250
- | `OPENCLAW_CONNECTOR_SECRET` | 里世界 APP → 我的龙虾 → 底部"**复制连接密钥**"按钮 |
251
- | `QCLAW_PATH` | QClaw 实际安装路径(通常右键桌面图标→属性→目标可以看到) |
252
-
253
- 如果 `OPENCLAW_CONNECTOR_SECRET` 没填或填错,程序会直接拒绝连接(提示"Connector 未授权")而不会启动。
254
-
255
- ### 第 5 步:扫码连接到里世界 APP
93
+ ### 3. 用里世界 APP 扫码
256
94
 
257
- 当连接器启动成功后,终端会打印:
95
+ 路径:
258
96
 
259
- - 当前 Runtime ID
260
- - QClaw 路径
261
- - OpenClaw 配置路径
262
- - 启动前检查结果
263
- - 一个二维码
97
+ - 打开里世界 APP
98
+ - 进入“我的龙虾”
99
+ - 点击“扫码连接本地龙虾”
100
+ - 扫描终端二维码
101
+ - 选择一只龙虾并确认绑定
264
102
 
265
- 这时在里世界 APP 里操作:
103
+ ### 4. 验证连接
266
104
 
267
- 1. 打开“我的龙虾”。
268
- 2. 找到“接入已有龙虾”。
269
- 3. 点击“扫码连接本地龙虾”。
270
- 4. 扫终端里的二维码。
271
- 5. 在确认页选择一只龙虾并确认。
105
+ 先发一句最短文本,例如:
272
106
 
273
- 如果你当前账号下还没有龙虾,APP 会允许你先创建一只,再直接完成连接。
107
+ - 请只回复:已收到
274
108
 
275
- ### 第 6 步:验证链路
109
+ 文本正常后,再测试截图或文件能力。
276
110
 
277
- 连接成功后,按下面顺序测:
111
+ ## .env 说明
278
112
 
279
- 1. 文本测试:`请只回复:已收到`
280
- 2. 普通问答:`帮我查北京明天的天气`
281
- 3. 截图测试:`帮我截图桌面`
282
- 4. 文件测试:让第三方程序返回结构化附件
113
+ .env 现在主要是为了补充本机路径和高级选项,不再是为了手填密钥。
283
114
 
284
- 建议永远先测短文本,再测截图,再测文件。这样最容易判断故障卡在哪一层。
115
+ ### 常用字段
285
116
 
286
- ## 连接器会输出哪些关键信息
117
+ | 字段 | 说明 |
118
+ | --- | --- |
119
+ | LICITY_API_BASE_URL | 服务器地址,通常保持 https://li.city |
120
+ | CONNECTOR_PROVIDER | 默认 openclaw-local,通常无需修改 |
121
+ | DEVICE_NAME | 当前电脑别名,扫码确认页会显示 |
122
+ | OPENCLAW_RUNTIME_PATH | 宿主 Runtime 主程序路径 |
123
+ | OPENCLAW_STATE_DIR | OpenClaw 状态目录 |
124
+ | OPENCLAW_CONFIG_PATH | OpenClaw 配置文件路径 |
125
+ | OPENCLAW_CLI_PATH | OpenClaw CLI 路径 |
126
+ | OPENCLAW_AGENT_ID | 默认 main |
127
+ | OPENCLAW_COMMAND_TIMEOUT_MS | 单次任务超时 |
128
+ | CAPABILITY_SCOPES | 功能域 |
287
129
 
288
- ### 1. 启动前检查
130
+ ### 兼容旧字段
289
131
 
290
- 终端会输出类似:
132
+ 如果你以前已经写过以下字段,不需要重配,仍然兼容:
291
133
 
292
- - OpenClaw 配置文件是否存在
293
- - OpenClaw CLI 是否存在
294
- - 模型网关地址
295
- - 19000 端口是否已监听
296
- - 微信通道地址
297
- - 网关探测是否成功
134
+ - QCLAW_PATH
135
+ - QCLAW_STATE_DIR
136
+ - QCLAW_CONFIG_PATH
137
+ - OPENCLAW_CONNECTOR_SECRET
138
+ - QCLAW_LLM_BASE_URL
139
+ - QCLAW_LLM_API_KEY
140
+ - QCLAW_WECHAT_WS_URL
298
141
 
299
- ### 2. 二维码会话
142
+ ## QClaw 用户说明
300
143
 
301
- 会输出类似:
144
+ 如果你使用的是 QClaw:
302
145
 
303
- - 新的连接会话 ID
304
- - 二维码原文 JSON
146
+ - 可以继续沿用现有安装路径和 .qclaw 目录。
147
+ - 连接器会自动优先兼容 QClaw 的 wrapper、qclaw.json 和网关配置。
148
+ - 包名虽然还是 qclaw-local-connector,但现在推荐你把它理解成“OpenClaw 通用连接器”。
305
149
 
306
- ### 3. 心跳日志
150
+ ## 其他 Runtime 用户说明
307
151
 
308
- 会输出类似:
152
+ 如果你不是 QClaw,而是其他兼容 OpenClaw CLI 的宿主:
309
153
 
310
- `[Heartbeat 3] 龙虾=3号龙虾 权限快照已同步`
154
+ - 重点保证 OPENCLAW_CONFIG_PATH 和 OPENCLAW_CLI_PATH 正确。
155
+ - 如宿主没有 qclaw.json,也不影响扫码连接。
156
+ - 如宿主网关不走 QClaw 变量,可直接设置:
157
+ - OPENCLAW_GATEWAY_BASE_URL
158
+ - OPENCLAW_GATEWAY_API_KEY
159
+ - OPENCLAW_WECHAT_WS_URL
311
160
 
312
- 说明本地连接器和后端之间的保活链路是通的。
161
+ ## 当前能力
313
162
 
314
- ### 4. 任务日志
163
+ 已经打通:
315
164
 
316
- 收到私聊任务时会输出:
165
+ - 扫码绑定
166
+ - 心跳保活
167
+ - 私聊文本回复
168
+ - 截图回传
169
+ - 图片 / 文件结构化回传
170
+ - 重新扫码接管旧连接
317
171
 
318
- `[Task] 收到任务 private_chat_message (...)`
172
+ ## 常见问题
319
173
 
320
- 如果进入普通问答,还会输出:
174
+ ### 1. 终端里出现二维码后,APP 扫码失败
321
175
 
322
- `[Agent] 开始调用 OpenClaw,agent=main,宿主=QClaw.exe,入口=openclaw.mjs,直连=yes,wrapper=bypassed,超时=60000ms`
176
+ 先检查:
323
177
 
324
- 如果最后出现:
178
+ - 服务器地址是否能访问
179
+ - 终端二维码是否是最新生成的
180
+ - 是否有旧连接器实例占着同一个 runtimeId
325
181
 
326
- `[Task] 私聊回写成功`
327
-
328
- 说明至少“里世界 -> 连接器 -> 本地执行 -> 回写里世界”这条主链路已经走通。
329
-
330
- ## 如何判断问题卡在哪一层
331
-
332
- ### A. APP 能发消息,但连接器终端完全没有任务日志
333
-
334
- 这说明任务压根没有派发到本地连接器。
182
+ ### 2. 连接成功但龙虾不回复
335
183
 
336
184
  优先检查:
337
185
 
338
- - 这只龙虾是否真的显示已连接。
339
- - 是否扫的是当前这台连接器终端打印的二维码。
340
- - 是否连错了账号。
341
- - 是否刚切换账号但还没重新扫码接管 Runtime。
342
-
343
- ### B. 终端出现 `[Task] 收到任务`,但没有后续执行日志
344
-
345
- 说明里世界到连接器链路是通的。
346
-
347
- 问题在本机执行层,通常是:
348
-
349
- - OpenClaw 配置异常。
350
- - QClaw 主程序未完全就绪。
351
- - 本地网关虽然监听,但 agent 无法真正拿到模型结果。
352
-
353
- ### C. 19000 端口已监听,但启动前检查显示 `Access denied (PID)`
354
-
355
- 这代表一个很具体的现象:
356
-
357
- - 你的 QClaw 本地代理端口确实起来了。
358
- - 但外部普通 Node 进程直连探测这个代理时,被本机 PID 策略拒绝。
359
-
360
- 这不一定等于完全不能用。
361
-
362
- 如果后续任务执行日志里显示:
363
-
364
- - `直连=yes`
365
- - `wrapper=bypassed`
366
- - `私聊回写成功`
367
-
368
- 说明虽然 HTTP 探测被拦截,但当前连接器已经通过更可信的宿主路径完成了调用,实际聊天链路仍然可能是可用的。
369
-
370
- ### D. 文本能回,截图能回,但文件回不来
371
-
372
- 这通常不是里世界 APP 的问题,而是第三方程序本身没有输出结构化附件。
373
-
374
- 当前连接器支持两种附件输入:
375
-
376
- 1. 直接给出 `media_base64`
377
- 2. 给出本地文件路径,连接器读取后转为 base64 回传
378
-
379
- 如果第三方 Runtime 只输出一段纯文本,例如“文件已生成到 D:\xxx”,但没有在结构化结果里真正带出文件字段,连接器就没法自动把那个文件回传到里世界。
380
-
381
- ### E. 换账号扫码失败或旧账号仍占着连接
382
-
383
- 当前后端已经支持 Runtime 接管。
384
-
385
- 正常行为应该是:
386
-
387
- - 新账号扫码成功后,旧账号上的同一 Runtime 绑定自动失效。
388
- - 不需要手动删数据库或重装连接器。
389
-
390
- 如果仍失败,优先重新生成二维码再扫一次,并确认 APP 当前登录的是正确账号。
391
-
392
- ## 文件和图片回传的经验总结
393
-
394
- 这一部分是本次接第三方龙虾最关键的经验。
395
-
396
- ### 1. 里世界数据库本身支持媒体字段,不是后端存不下
397
-
398
- 真正的短板往往不是数据库,而是“第三方 Runtime 有没有把媒体按规范回出来”。
186
+ - 本机 Runtime 是否真的启动
187
+ - OpenClaw CLI 是否可执行
188
+ - 本地模型/网关端口是否已监听
399
189
 
400
- ### 2. 截图类任务最好在连接器侧直接做
190
+ ### 3. 以前必须填的 OPENCLAW_CONNECTOR_SECRET 现在还要填吗
401
191
 
402
- 例如“帮我截图桌面”这种需求,连接器可以直接本地截图,然后把图片回传给里世界。
192
+ 通常不需要。
403
193
 
404
- 这样最稳定,因为:
194
+ 它现在只作为旧版兼容字段保留。正常扫码绑定流程下,连接器会自动拿到本机专用 token。
405
195
 
406
- - 不依赖模型自己理解如何上传文件。
407
- - 不依赖第三方程序输出复杂附件格式。
196
+ ## 发布说明
408
197
 
409
- ### 3. 普通文件回传,关键在结构化附件
410
-
411
- 如果你要让第三方程序回传文件,建议它在结构化结果里带出至少这些字段之一:
412
-
413
- - `media_base64`
414
- - `media_file_path`
415
- - `file_base64`
416
- - `file_path`
417
-
418
- 以及这些辅助字段:
419
-
420
- - `media_name`
421
- - `media_mime_type`
422
- - `media_type`
423
-
424
- ### 4. 附件不要太大
425
-
426
- 当前连接器默认走 JSON 请求把附件回传到后端,建议单个附件控制在 8MB 以内。
427
-
428
- 如果你后续要走更大的文件,建议改成先上传对象存储,再只回传 URL。
429
-
430
- ## 推荐的标准操作顺序
431
-
432
- 1. 先启动 QClaw 主程序。
433
- 2. 用 `Get-NetTCPConnection` 确认 19000 已监听。
434
- 3. 进入本目录,执行 `npm install && npm run quickstart`。
435
- 4. 看安装助手是否通过关键检查。
436
- 5. 看连接器终端是否打印二维码。
437
- 6. 去 APP 里扫码绑定。
438
- 7. 先测短文本。
439
- 8. 再测截图。
440
- 9. 最后再测文件。
441
-
442
- ## 常用命令
443
-
444
- ### npm 全局安装用户
445
-
446
- ```bash
447
- # 直接在 .env 文件所在目录运行
448
- licity-connector
449
- ```
450
-
451
- ### 本地克隆 / 下载用户
198
+ 当前推荐安装源仍然是:
452
199
 
453
200
  ```bash
454
- # 一键准备并启动
455
- npm install && npm run quickstart
456
-
457
- # 只做环境自检
458
- npm run doctor
459
-
460
- # 只启动连接器
461
- npm run start
462
- ```
463
-
464
- ### 手工执行 OpenClaw 自检(Windows PowerShell)
465
-
466
- 把下面的路径替换成你实际的 QClaw 安装路径和状态目录:
467
-
468
- ```powershell
469
- $env:OPENCLAW_CONFIG_PATH="$env:USERPROFILE\.qclaw\openclaw.json"
470
- $env:QCLAW_LLM_BASE_URL='http://127.0.0.1:19000/proxy'
471
- $env:QCLAW_LLM_API_KEY='<从 openclaw.json 中获取的 apiKey>'
472
- $env:QCLAW_WECHAT_WS_URL='ws://127.0.0.1:19000/proxy'
473
- node '<QClaw安装目录>\resources\openclaw\node_modules\openclaw\openclaw.mjs' agent --agent main --message '请只回复四个字:已收到。不要解释。' --json --timeout 30
201
+ npm install -g @licity/qclaw-local-connector
474
202
  ```
475
203
 
476
- 如果这条命令都不能正常返回,问题就不在里世界,也不在这个连接器,而在你本机 QClaw/OpenClaw 环境本身。
477
-
478
- ## 最后的建议
479
-
480
- 如果你是小白,别一上来就测复杂问题。
481
-
482
- 永远按这个顺序:
483
-
484
- 1. 先看 19000 端口。
485
- 2. 再看连接器能不能出二维码。
486
- 3. 再看 APP 能不能扫码成功。
487
- 4. 再测短文本。
488
- 5. 再测截图。
489
- 6. 最后再测文件。
490
-
491
- 按这个顺序,你几乎总能很快定位问题卡在哪一层。
204
+ 这是兼容升级,不是废弃重发。旧用户无需改包名,新的教程和命令会直接按 OpenClaw 通用连接器来写。
package/index.js CHANGED
@@ -17,17 +17,47 @@ require('dotenv').config({ path: fs.existsSync(envFromCwd) ? envFromCwd : envFro
17
17
 
18
18
  const pkg = require('./package.json');
19
19
 
20
+ function findFirstExistingPath(candidates, fallback = '') {
21
+ for (const candidate of candidates) {
22
+ const resolved = String(candidate || '').trim();
23
+ if (resolved && fs.existsSync(resolved)) {
24
+ return resolved;
25
+ }
26
+ }
27
+ return String(fallback || '').trim();
28
+ }
29
+
30
+ const detectedRuntimePath = findFirstExistingPath(
31
+ [
32
+ process.env.OPENCLAW_RUNTIME_PATH,
33
+ process.env.QCLAW_PATH,
34
+ 'D:\\QClaw\\QClaw.exe',
35
+ 'C:\\QClaw\\QClaw.exe',
36
+ ],
37
+ process.env.OPENCLAW_RUNTIME_PATH || process.env.QCLAW_PATH || 'D:\\QClaw\\QClaw.exe'
38
+ );
39
+ const detectedRuntimeStateDir = findFirstExistingPath(
40
+ [
41
+ process.env.OPENCLAW_STATE_DIR,
42
+ process.env.QCLAW_STATE_DIR,
43
+ path.join(os.homedir(), '.qclaw'),
44
+ process.env.USERPROFILE ? path.join(process.env.USERPROFILE, '.qclaw') : '',
45
+ ],
46
+ path.join(os.homedir(), '.qclaw')
47
+ );
48
+
20
49
  const apiBaseUrl = String(process.env.LICITY_API_BASE_URL || 'https://li.city').replace(/\/$/, '');
21
50
  const connectorKey = String(process.env.OPENCLAW_CONNECTOR_SECRET || '').trim();
22
- const provider = String(process.env.CONNECTOR_PROVIDER || 'qclaw-local').trim();
51
+ const provider = String(process.env.CONNECTOR_PROVIDER || 'openclaw-local').trim();
23
52
  const deviceName = String(process.env.DEVICE_NAME || os.hostname()).trim();
24
- const qclawPath = String(process.env.QCLAW_PATH || 'D:\\QClaw\\QClaw.exe').trim();
25
- const qclawInstallDir = path.dirname(qclawPath);
26
- const qclawCliWrapperPath = path.join(qclawInstallDir, 'resources', 'openclaw', 'config', 'skills', 'qclaw-openclaw', 'scripts', 'openclaw-win.cmd');
27
- const qclawStateDir = String(process.env.QCLAW_STATE_DIR || path.join(os.homedir(), '.qclaw')).trim();
28
- const qclawConfigPath = String(process.env.QCLAW_CONFIG_PATH || path.join(qclawStateDir, 'qclaw.json')).trim();
29
- const openclawConfigPath = String(process.env.OPENCLAW_CONFIG_PATH || path.join(qclawStateDir, 'openclaw.json')).trim();
30
- const openclawCliPath = String(process.env.OPENCLAW_CLI_PATH || path.join(qclawInstallDir, 'resources', 'openclaw', 'node_modules', 'openclaw', 'openclaw.mjs')).trim();
53
+ const runtimePath = String(detectedRuntimePath).trim();
54
+ const runtimeInstallDir = path.dirname(runtimePath);
55
+ const runtimeDisplayName = String(process.env.OPENCLAW_RUNTIME_NAME || 'OpenClaw Runtime').trim();
56
+ const qclawCliWrapperPath = path.join(runtimeInstallDir, 'resources', 'openclaw', 'config', 'skills', 'qclaw-openclaw', 'scripts', 'openclaw-win.cmd');
57
+ const runtimeStateDir = String(detectedRuntimeStateDir).trim();
58
+ const qclawConfigPath = String(process.env.QCLAW_CONFIG_PATH || path.join(runtimeStateDir, 'qclaw.json')).trim();
59
+ const openclawConfigPath = String(process.env.OPENCLAW_CONFIG_PATH || path.join(runtimeStateDir, 'openclaw.json')).trim();
60
+ const openclawCliPath = String(process.env.OPENCLAW_CLI_PATH || path.join(runtimeInstallDir, 'resources', 'openclaw', 'node_modules', 'openclaw', 'openclaw.mjs')).trim();
31
61
  const openclawAgentId = String(process.env.OPENCLAW_AGENT_ID || 'main').trim();
32
62
  const openclawCommandTimeoutMs = Number(process.env.OPENCLAW_COMMAND_TIMEOUT_MS || 60000);
33
63
  const heartbeatIntervalMs = Number(process.env.HEARTBEAT_INTERVAL_MS || 25000);
@@ -39,38 +69,22 @@ const capabilityScopes = String(process.env.CAPABILITY_SCOPES || 'private_chat,n
39
69
  .filter(Boolean);
40
70
  const execFileAsync = promisify(execFile);
41
71
 
42
- if (!connectorKey) {
43
- // 当工作目录没有 .env 时,自动生成模板并引导用户
44
- if (!fs.existsSync(envFromCwd)) {
45
- const templateDest = path.join(process.cwd(), '.env');
46
- const templateContent = fs.existsSync(envExamplePath)
47
- ? fs.readFileSync(envExamplePath, 'utf8')
48
- : [
49
- 'LICITY_API_BASE_URL=https://li.city',
50
- 'OPENCLAW_CONNECTOR_SECRET=',
51
- 'QCLAW_PATH=C:\\QClaw\\QClaw.exe',
52
- `QCLAW_STATE_DIR=${path.join(os.homedir(), '.qclaw')}`,
53
- `OPENCLAW_CONFIG_PATH=${path.join(os.homedir(), '.qclaw', 'openclaw.json')}`,
54
- ].join('\n') + '\n';
55
- fs.writeFileSync(templateDest, templateContent, 'utf8');
56
- console.error('');
57
- console.error('══════════════════════════════════════════════════════');
58
- console.error(' 首次运行:已在当前目录生成 .env 配置文件模板');
59
- console.error(` 文件位置:${templateDest}`);
60
- console.error('');
61
- console.error(' 请用文本编辑器打开 .env,填写以下必填项:');
62
- console.error(' OPENCLAW_CONNECTOR_SECRET — 从里世界 APP「我的龙虾」页');
63
- console.error(' 复制「连接密钥」后粘贴到这里');
64
- console.error(' QCLAW_PATH — QClaw 主程序实际路径');
65
- console.error(' QCLAW_STATE_DIR — QClaw 数据目录(含用户名)');
66
- console.error('');
67
- console.error(' 填写完毕后在同一目录重新运行:licity-connector');
68
- console.error('══════════════════════════════════════════════════════');
69
- console.error('');
70
- } else {
71
- console.error('缺少 OPENCLAW_CONNECTOR_SECRET,请在当前目录的 .env 文件中填写连接密钥。');
72
- }
73
- process.exit(1);
72
+ if (!fs.existsSync(envFromCwd)) {
73
+ const templateDest = path.join(process.cwd(), '.env');
74
+ const templateContent = (fs.existsSync(envExamplePath)
75
+ ? fs.readFileSync(envExamplePath, 'utf8')
76
+ : '')
77
+ .replace(/OPENCLAW_RUNTIME_PATH=.*/g, `OPENCLAW_RUNTIME_PATH=${runtimePath || 'D:\\QClaw\\QClaw.exe'}`)
78
+ .replace(/QCLAW_PATH=.*/g, `QCLAW_PATH=${runtimePath || 'D:\\QClaw\\QClaw.exe'}`)
79
+ .replace(/OPENCLAW_STATE_DIR=.*/g, `OPENCLAW_STATE_DIR=${runtimeStateDir}`)
80
+ .replace(/QCLAW_STATE_DIR=.*/g, `QCLAW_STATE_DIR=${runtimeStateDir}`)
81
+ .replace(/QCLAW_CONFIG_PATH=.*/g, `QCLAW_CONFIG_PATH=${qclawConfigPath}`)
82
+ .replace(/OPENCLAW_CONFIG_PATH=.*/g, `OPENCLAW_CONFIG_PATH=${openclawConfigPath}`)
83
+ .replace(/OPENCLAW_CLI_PATH=.*/g, `OPENCLAW_CLI_PATH=${openclawCliPath}`);
84
+ fs.writeFileSync(templateDest, templateContent, 'utf8');
85
+ console.log(`首次运行已在当前目录生成可选 .env: ${templateDest}`);
86
+ console.log('未填写 OPENCLAW_CONNECTOR_SECRET 也可以继续:程序会先出二维码,扫码通过后自动保存本机连接令牌。');
87
+ console.log('');
74
88
  }
75
89
 
76
90
  const state = {
@@ -80,6 +94,7 @@ const state = {
80
94
  currentSessionId: null,
81
95
  currentLobster: null,
82
96
  heartbeatCount: 0,
97
+ connectorToken: '',
83
98
  };
84
99
 
85
100
  // 全局安装时数据目录在运行目录下,本地开发时在包目录的 data/
@@ -89,32 +104,53 @@ const dataDir = isGlobalInstall
89
104
  : path.join(__dirname, 'data');
90
105
  const runtimeFile = path.join(dataDir, 'runtime.json');
91
106
 
92
- function ensureRuntimeId() {
107
+ function readRuntimeState() {
93
108
  fs.mkdirSync(dataDir, { recursive: true });
94
109
 
95
110
  if (fs.existsSync(runtimeFile)) {
96
111
  try {
97
- const saved = JSON.parse(fs.readFileSync(runtimeFile, 'utf8'));
98
- if (saved && typeof saved.runtimeId === 'string' && saved.runtimeId.trim()) {
99
- return saved.runtimeId.trim();
100
- }
112
+ return JSON.parse(fs.readFileSync(runtimeFile, 'utf8')) || {};
101
113
  } catch (error) {
102
- console.warn('读取本地 runtime.json 失败,将重新生成 runtimeId。');
114
+ console.warn('读取本地 runtime.json 失败,将重新生成本地运行状态。');
103
115
  }
104
116
  }
105
117
 
106
- const runtimeId = crypto.randomUUID();
107
- fs.writeFileSync(runtimeFile, JSON.stringify({ runtimeId, createdAt: new Date().toISOString() }, null, 2));
108
- return runtimeId;
118
+ return {};
119
+ }
120
+
121
+ function writeRuntimeState(runtimeState) {
122
+ fs.mkdirSync(dataDir, { recursive: true });
123
+ fs.writeFileSync(runtimeFile, JSON.stringify(runtimeState, null, 2));
124
+ }
125
+
126
+ const runtimeState = readRuntimeState();
127
+ if (!runtimeState.runtimeId || !String(runtimeState.runtimeId).trim()) {
128
+ runtimeState.runtimeId = crypto.randomUUID();
129
+ }
130
+ if (!runtimeState.createdAt) {
131
+ runtimeState.createdAt = new Date().toISOString();
132
+ }
133
+ writeRuntimeState(runtimeState);
134
+
135
+ function persistConnectorToken(token) {
136
+ runtimeState.connectorToken = String(token || '').trim();
137
+ runtimeState.updatedAt = new Date().toISOString();
138
+ writeRuntimeState(runtimeState);
139
+ state.connectorToken = runtimeState.connectorToken;
109
140
  }
110
141
 
111
- const runtimeId = ensureRuntimeId();
142
+ const runtimeId = String(runtimeState.runtimeId).trim();
143
+ state.connectorToken = String(runtimeState.connectorToken || '').trim();
112
144
  const screenshotDir = path.join(dataDir, 'screenshots');
113
145
 
114
146
  function sleep(ms) {
115
147
  return new Promise((resolve) => setTimeout(resolve, ms));
116
148
  }
117
149
 
150
+ function sha256(value) {
151
+ return crypto.createHash('sha256').update(String(value || '')).digest('hex');
152
+ }
153
+
118
154
  function ensureDir(dirPath) {
119
155
  fs.mkdirSync(dirPath, { recursive: true });
120
156
  }
@@ -147,21 +183,25 @@ function toWebSocketUrl(rawUrl) {
147
183
  function buildOpenClawChildEnv() {
148
184
  const qclawConfig = readJsonFileIfExists(qclawConfigPath) || {};
149
185
  const openclawConfig = readJsonFileIfExists(openclawConfigPath) || {};
150
- const qclawGatewayBaseUrl = String(qclawConfig.authGatewayBaseUrl || '').trim();
186
+ const qclawGatewayBaseUrl = String(qclawConfig.authGatewayBaseUrl || openclawConfig.authGatewayBaseUrl || '').trim();
151
187
  const gatewayToken = String(openclawConfig?.gateway?.auth?.token || '').trim();
152
- const modelBaseUrl = String(process.env.QCLAW_LLM_BASE_URL || qclawGatewayBaseUrl).trim();
153
- const modelApiKey = String(process.env.QCLAW_LLM_API_KEY || gatewayToken).trim();
154
- const wechatWsUrl = String(process.env.QCLAW_WECHAT_WS_URL || toWebSocketUrl(qclawGatewayBaseUrl)).trim();
188
+ const modelBaseUrl = String(process.env.OPENCLAW_GATEWAY_BASE_URL || process.env.OPENCLAW_LLM_BASE_URL || process.env.QCLAW_LLM_BASE_URL || qclawGatewayBaseUrl).trim();
189
+ const modelApiKey = String(process.env.OPENCLAW_GATEWAY_API_KEY || process.env.OPENCLAW_LLM_API_KEY || process.env.QCLAW_LLM_API_KEY || gatewayToken).trim();
190
+ const wechatWsUrl = String(process.env.OPENCLAW_WECHAT_WS_URL || process.env.QCLAW_WECHAT_WS_URL || toWebSocketUrl(qclawGatewayBaseUrl)).trim();
155
191
 
156
192
  return {
157
193
  ...process.env,
158
194
  OPENCLAW_CONFIG_PATH: openclawConfigPath,
159
- OPENCLAW_STATE_DIR: qclawStateDir,
195
+ OPENCLAW_STATE_DIR: runtimeStateDir,
160
196
  OPENCLAW_NIX_MODE: '1',
161
197
  NODE_OPTIONS: '--no-warnings',
198
+ ...(runtimePath ? { OPENCLAW_RUNTIME_PATH: runtimePath } : {}),
162
199
  ...(modelBaseUrl ? { QCLAW_LLM_BASE_URL: modelBaseUrl } : {}),
163
200
  ...(modelApiKey ? { QCLAW_LLM_API_KEY: modelApiKey } : {}),
164
201
  ...(wechatWsUrl ? { QCLAW_WECHAT_WS_URL: wechatWsUrl } : {}),
202
+ ...(modelBaseUrl ? { OPENCLAW_LLM_BASE_URL: modelBaseUrl } : {}),
203
+ ...(modelApiKey ? { OPENCLAW_LLM_API_KEY: modelApiKey } : {}),
204
+ ...(wechatWsUrl ? { OPENCLAW_WECHAT_WS_URL: wechatWsUrl } : {}),
165
205
  };
166
206
  }
167
207
 
@@ -267,13 +307,22 @@ async function runPreflightChecks() {
267
307
 
268
308
  async function requestJson(endpoint, options = {}) {
269
309
  const url = `${apiBaseUrl}${endpoint}`;
310
+ const headers = {
311
+ 'Content-Type': 'application/json',
312
+ ...(options.headers || {}),
313
+ };
314
+
315
+ if (!options.allowAnonymous) {
316
+ if (connectorKey) {
317
+ headers['x-connector-key'] = connectorKey;
318
+ } else if (state.connectorToken) {
319
+ headers['x-connector-token'] = state.connectorToken;
320
+ }
321
+ }
322
+
270
323
  const response = await fetch(url, {
271
324
  method: options.method || 'GET',
272
- headers: {
273
- 'Content-Type': 'application/json',
274
- 'x-connector-key': connectorKey,
275
- ...(options.headers || {}),
276
- },
325
+ headers,
277
326
  body: options.body ? JSON.stringify(options.body) : undefined,
278
327
  });
279
328
 
@@ -299,13 +348,15 @@ async function requestJson(endpoint, options = {}) {
299
348
 
300
349
  function printBanner() {
301
350
  console.log('========================================');
302
- console.log(' 里世界 QClaw 本地龙虾 Connector');
351
+ console.log(' 里世界 OpenClaw 本地龙虾 Connector');
303
352
  console.log('========================================');
304
353
  console.log(`API: ${apiBaseUrl}`);
305
354
  console.log(`Provider: ${provider}`);
306
355
  console.log(`Runtime ID: ${runtimeId}`);
307
356
  console.log(`设备名: ${deviceName}`);
308
- console.log(`QClaw 路径: ${qclawPath}`);
357
+ console.log(`鉴权模式: ${connectorKey ? '兼容旧版密钥' : (state.connectorToken ? '本机扫码令牌' : '首次扫码自动授权')}`);
358
+ console.log(`Runtime: ${runtimeDisplayName}`);
359
+ console.log(`Runtime 路径: ${runtimePath}`);
309
360
  console.log(`OpenClaw 配置: ${openclawConfigPath}`);
310
361
  console.log(`能力域: ${capabilityScopes.join(', ') || '无'}`);
311
362
  console.log('命令: 输入 r 重新生成二维码,输入 s 查看状态,输入 q 退出');
@@ -330,10 +381,10 @@ function printPreflightResult(preflight) {
330
381
  }
331
382
  }
332
383
  if (preflight.modelPort && !preflight.modelPortReady) {
333
- console.log('! 检测到本地模型端口未启动。仅启动连接器不够,还需要先启动 QClaw 主程序并保持其本地网关可用。');
384
+ console.log(`! 检测到本地模型端口未启动。仅启动连接器不够,还需要先启动 ${runtimeDisplayName} 并保持其本地网关可用。`);
334
385
  }
335
386
  if (preflight.gatewayProbe?.message && String(preflight.gatewayProbe.message).includes('Access denied (PID)')) {
336
- console.log('! 当前连接器进程访问 QClaw 模型代理时被本机 PID 策略拒绝。');
387
+ console.log(`! 当前连接器进程访问 ${runtimeDisplayName} 模型代理时被本机 PID 策略拒绝。`);
337
388
  console.log('! 这说明端口虽然已监听,但普通问答任务仍无法由外部 node 进程真正执行。');
338
389
  }
339
390
  console.log('');
@@ -607,13 +658,13 @@ async function runOpenClawAgent(task) {
607
658
  '--timeout',
608
659
  String(Math.max(15, Math.ceil(openclawCommandTimeoutMs / 1000))),
609
660
  ], {
610
- cwd: qclawStateDir,
661
+ cwd: runtimeStateDir,
611
662
  env: childEnv,
612
663
  windowsHide: true,
613
664
  stdio: ['ignore', 'pipe', 'pipe'],
614
665
  })
615
666
  : spawn(command, args, {
616
- cwd: qclawStateDir,
667
+ cwd: runtimeStateDir,
617
668
  env: childEnv,
618
669
  windowsHide: true,
619
670
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -731,9 +782,12 @@ async function createScanSession() {
731
782
  state.currentSessionId = null;
732
783
  state.currentLobster = null;
733
784
  state.heartbeatCount = 0;
785
+ const pollToken = crypto.randomUUID();
786
+ state.currentPollToken = pollToken;
734
787
 
735
788
  return requestJson('/api/openclaw/scan-session', {
736
789
  method: 'POST',
790
+ allowAnonymous: true,
737
791
  body: {
738
792
  runtimeId,
739
793
  provider,
@@ -742,9 +796,10 @@ async function createScanSession() {
742
796
  version: pkg.version,
743
797
  capabilityScopes,
744
798
  metadata: {
745
- connector: 'licity-qclaw-local-connector',
799
+ connector: 'licity-openclaw-local-connector',
746
800
  nodeVersion: process.version,
747
- qclawPath,
801
+ runtimePath,
802
+ pollTokenHash: sha256(pollToken),
748
803
  },
749
804
  },
750
805
  });
@@ -753,12 +808,19 @@ async function createScanSession() {
753
808
  async function waitForApproval(sessionId) {
754
809
  state.mode = 'waiting-approval';
755
810
  state.currentSessionId = sessionId;
811
+ const pollToken = state.currentPollToken;
756
812
 
757
813
  while (state.shouldRun && !state.reconnectRequested) {
758
- const result = await requestJson(`/api/openclaw/scan-session/${sessionId}`);
814
+ const result = await requestJson(
815
+ `/api/openclaw/scan-session/${sessionId}?pollToken=${encodeURIComponent(pollToken)}`,
816
+ { allowAnonymous: true }
817
+ );
759
818
  const session = result.session;
760
819
 
761
820
  if (session.status === 'approved') {
821
+ if (session.connectorAuthToken) {
822
+ persistConnectorToken(session.connectorAuthToken);
823
+ }
762
824
  state.mode = 'connected';
763
825
  state.currentLobster = session.lobster || null;
764
826
  return session;
@@ -784,8 +846,8 @@ async function sendHeartbeat() {
784
846
  body: {
785
847
  runtimeId,
786
848
  metadata: {
787
- connector: 'licity-qclaw-local-connector',
788
- qclawPath,
849
+ connector: 'licity-openclaw-local-connector',
850
+ runtimePath,
789
851
  heartbeatAt: new Date().toISOString(),
790
852
  },
791
853
  },
@@ -820,7 +882,7 @@ async function emitTaskReply(task, payloadOrContent) {
820
882
  meta: {
821
883
  taskId: task.id,
822
884
  runtimeId,
823
- qclawPath,
885
+ runtimePath,
824
886
  },
825
887
  }
826
888
  : {
@@ -829,7 +891,7 @@ async function emitTaskReply(task, payloadOrContent) {
829
891
  ...(payloadOrContent?.meta || {}),
830
892
  taskId: task.id,
831
893
  runtimeId,
832
- qclawPath,
894
+ runtimePath,
833
895
  },
834
896
  };
835
897
 
@@ -856,7 +918,7 @@ async function handleTask(task) {
856
918
  const brief = rawContent ? rawContent.slice(0, 80) : '空消息';
857
919
  let reply = '';
858
920
  let taskResult = {
859
- qclawPath,
921
+ runtimePath,
860
922
  openclawAgentId,
861
923
  };
862
924
 
@@ -957,6 +1019,9 @@ async function heartbeatLoop() {
957
1019
  } catch (error) {
958
1020
  console.error(`心跳失败: ${error.message}`);
959
1021
  if (error.status === 403 || error.status === 404) {
1022
+ if (!connectorKey) {
1023
+ persistConnectorToken('');
1024
+ }
960
1025
  console.log('当前绑定已失效,准备重新进入扫码连接。');
961
1026
  return;
962
1027
  }
@@ -975,6 +1040,9 @@ async function heartbeatLoop() {
975
1040
  } catch (error) {
976
1041
  console.error(`拉取任务失败: ${error.message}`);
977
1042
  if (error.status === 403 || error.status === 404) {
1043
+ if (!connectorKey) {
1044
+ persistConnectorToken('');
1045
+ }
978
1046
  console.log('当前绑定已失效,准备重新进入扫码连接。');
979
1047
  return;
980
1048
  }
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@licity/qclaw-local-connector",
3
- "version": "1.3.2",
4
- "description": "里世界龙虾本地连接器 QClaw 或其他第三方本地 Runtime 接入里世界 APP",
3
+ "version": "1.4.0",
4
+ "description": "里世界 OpenClaw 本地连接器,支持 QClaw 与其他兼容 OpenClaw CLI 的本地 Runtime 扫码接入",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "licity-connector": "index.js"
7
+ "licity-connector": "index.js",
8
+ "licity-openclaw-connector": "index.js"
8
9
  },
9
10
  "files": [
10
11
  "index.js",
11
12
  "setup.js",
12
13
  ".env.example",
13
- "README.md"
14
+ "README.md",
15
+ "启动里世界OpenClaw连接器.bat"
14
16
  ],
15
17
  "scripts": {
16
18
  "setup": "node setup.js",
@@ -22,6 +24,7 @@
22
24
  "licity",
23
25
  "qclaw",
24
26
  "openclaw",
27
+ "runtime",
25
28
  "connector",
26
29
  "lobster",
27
30
  "local-connector"
package/setup.js CHANGED
@@ -27,6 +27,16 @@ function ensureDir(dirPath) {
27
27
  fs.mkdirSync(dirPath, { recursive: true });
28
28
  }
29
29
 
30
+ function findFirstExistingPath(candidates, fallback = '') {
31
+ for (const candidate of candidates) {
32
+ const resolved = String(candidate || '').trim();
33
+ if (resolved && fs.existsSync(resolved)) {
34
+ return resolved;
35
+ }
36
+ }
37
+ return String(fallback || '').trim();
38
+ }
39
+
30
40
  function ensureEnvFile() {
31
41
  if (fs.existsSync(envPath)) {
32
42
  return false;
@@ -47,32 +57,33 @@ function main() {
47
57
  const isDoctorOnly = process.argv.includes('--doctor');
48
58
  const isPrepareOnly = process.argv.includes('--prepare-only');
49
59
 
50
- console.log('里世界 QClaw 连接器安装助手');
60
+ console.log('里世界 OpenClaw 连接器安装助手');
51
61
  const createdEnv = ensureEnvFile();
52
62
  ensureDir(dataDir);
53
63
  ensureDir(screenshotDir);
54
64
 
55
65
  const env = readEnvFile(envPath);
56
- const qclawPath = String(env.QCLAW_PATH || 'D:\\QClaw\\QClaw.exe').trim();
57
- const qclawStateDir = String(env.QCLAW_STATE_DIR || path.join(os.homedir(), '.qclaw')).trim();
58
- const openclawConfigPath = String(env.OPENCLAW_CONFIG_PATH || path.join(qclawStateDir, 'openclaw.json')).trim();
66
+ const runtimePath = findFirstExistingPath(
67
+ [env.OPENCLAW_RUNTIME_PATH, env.QCLAW_PATH, 'D:\\QClaw\\QClaw.exe', 'C:\\QClaw\\QClaw.exe'],
68
+ env.OPENCLAW_RUNTIME_PATH || env.QCLAW_PATH || 'D:\\QClaw\\QClaw.exe'
69
+ );
70
+ const runtimeStateDir = findFirstExistingPath(
71
+ [env.OPENCLAW_STATE_DIR, env.QCLAW_STATE_DIR, path.join(os.homedir(), '.qclaw')],
72
+ path.join(os.homedir(), '.qclaw')
73
+ );
74
+ const openclawConfigPath = String(env.OPENCLAW_CONFIG_PATH || path.join(runtimeStateDir, 'openclaw.json')).trim();
59
75
  const connectorSecret = String(env.OPENCLAW_CONNECTOR_SECRET || '').trim();
60
76
 
61
- printCheck('.env 文件', true, createdEnv ? '已按模板创建,请补全密钥后再启动' : '已存在');
77
+ printCheck('.env 文件', true, createdEnv ? '已按模板创建,可直接扫码授权' : '已存在');
62
78
  printCheck('数据目录', true, dataDir);
63
79
  printCheck('截图目录', true, screenshotDir);
64
- printCheck('QClaw 程序路径', fs.existsSync(qclawPath), qclawPath);
80
+ printCheck('Runtime 程序路径', fs.existsSync(runtimePath), runtimePath);
65
81
  printCheck('OpenClaw 配置文件', fs.existsSync(openclawConfigPath), openclawConfigPath);
66
- printCheck('连接器密钥', !!connectorSecret, connectorSecret ? '已配置' : '请在 .env 中填写 OPENCLAW_CONNECTOR_SECRET');
82
+ printCheck('连接器密钥', true, connectorSecret ? '已配置旧版兼容密钥' : '未配置,将在扫码成功后自动下发本机令牌');
67
83
 
68
84
  if (isDoctorOnly || isPrepareOnly) {
69
85
  return;
70
86
  }
71
-
72
- if (!connectorSecret) {
73
- console.log('未检测到连接器密钥,已停止。先打开 .env 填入 OPENCLAW_CONNECTOR_SECRET,再执行 npm run quickstart。');
74
- process.exit(1);
75
- }
76
87
  }
77
88
 
78
89
  main();
@@ -0,0 +1,31 @@
1
+ @echo off
2
+ setlocal
3
+ chcp 65001 >nul
4
+ title 里世界 OpenClaw 本地连接器
5
+
6
+ echo ========================================
7
+ echo 里世界 OpenClaw 本地连接器
8
+ echo ========================================
9
+ echo 首次运行会自动生成 .env,并直接显示二维码。
10
+ echo 扫码通过后会自动下发本机连接令牌,无需手填密钥。
11
+ echo.
12
+
13
+ where licity-openclaw-connector >nul 2>nul
14
+ if %errorlevel%==0 (
15
+ licity-openclaw-connector
16
+ goto end
17
+ )
18
+
19
+ where licity-connector >nul 2>nul
20
+ if %errorlevel%==0 (
21
+ licity-connector
22
+ goto end
23
+ )
24
+
25
+ echo 未找到连接器命令,请先执行:
26
+ echo npm install -g @licity/qclaw-local-connector
27
+ echo.
28
+ pause
29
+
30
+ :end
31
+ endlocal