@licity/qclaw-local-connector 1.3.3 → 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,5 +1,5 @@
1
1
  # ──────────────────────────────────────────────────────────────────────────────
2
- # 里世界 QClaw 本地连接器 · .env 配置文件
2
+ # 里世界 OpenClaw 本地连接器 · .env 配置文件
3
3
  # 用任意文本编辑器(如记事本)打开,按说明填写后保存,重新运行 licity-connector。
4
4
  # 带【勿改】标记的字段直接保留默认值;带【必填】的必须根据你的电脑情况修改。
5
5
  # ──────────────────────────────────────────────────────────────────────────────
@@ -7,35 +7,40 @@
7
7
  # 【勿改】里世界服务器地址,固定不动。
8
8
  LICITY_API_BASE_URL=https://li.city
9
9
 
10
- # 【必填】连接器验证密钥。
11
- # 获取方式:打开里世界 APP → 我的龙虾 → 滑到底部 → 点「复制连接密钥」按钮 → 粘贴到等号后面。
12
- # 填错或留空时,程序启动后会立即报错退出。
13
- OPENCLAW_CONNECTOR_SECRET=replace-with-your-connector-secret
10
+ # 【可选】旧版连接器验证密钥。
11
+ # 新版推荐直接留空:首次运行会先显示二维码,扫码通过后自动下发并保存本机连接令牌。
12
+ # 只有你要兼容旧版脚本或手动调用受保护接口时,才需要填写这个全局密钥。
13
+ OPENCLAW_CONNECTOR_SECRET=
14
14
 
15
- # 【勿改】连接器类型标识,告诉里世界服务器这是 QClaw 本地连接器,固定不动。
16
- CONNECTOR_PROVIDER=qclaw-local
15
+ # 【勿改】连接器类型标识,告诉里世界服务器这是 OpenClaw 本地连接器,固定不动。
16
+ CONNECTOR_PROVIDER=openclaw-local
17
17
 
18
18
  # 【可选】这台电脑的别名,多台电脑接入时方便区分。
19
19
  # 留空时自动使用电脑的主机名(hostname)。可以填中文,例如:我的台式机
20
20
  DEVICE_NAME=
21
21
 
22
22
  # ──────────────────────────────────────────────────────────────────────────────
23
- # QClaw 路径相关(以下四项都需要根据你的电脑实际情况填写)
23
+ # Runtime 路径相关(以下字段优先兼容 OpenClaw,也兼容 QClaw
24
24
  # ──────────────────────────────────────────────────────────────────────────────
25
25
 
26
- # 【必填】QClaw 主程序(.exe)的完整路径。
27
- # 怎么找:右键点击 QClaw 快捷方式 → 属性 → 「目标」框内的路径。
28
- # 常见路径:C:\QClaw\QClaw.exe 或 D:\QClaw\QClaw.exe(视安装目录而定)
26
+ # 【可选】宿主 Runtime 主程序(.exe)完整路径。
27
+ # 如果你使用 QClaw,可直接填 QClaw 主程序路径;其他宿主可填其实际启动文件。
28
+ OPENCLAW_RUNTIME_PATH=D:\QClaw\QClaw.exe
29
+
30
+ # 兼容旧版字段。若你已经在用 QCLAW_PATH,可继续保留。
29
31
  QCLAW_PATH=D:\QClaw\QClaw.exe
30
32
 
31
- # 【必填】QClaw 数据目录(存放配置文件、登录状态等)。
33
+ # 【必填】OpenClaw / 宿主 Runtime 的状态目录(存放配置文件、登录状态等)。
32
34
  # ⚠️ 必须把 Administrator 改成你电脑的 Windows 用户名(中文名也可以)!
33
35
  # 怎么查用户名:按 Win+R → 输入 cmd 回车 → 输入 echo %USERNAME% → 回车查看。
34
36
  # 确认方法:打开文件管理器 → 地址栏输入此路径 → 能看到文件夹说明路径正确。
37
+ OPENCLAW_STATE_DIR=C:\Users\Administrator\.qclaw
38
+
39
+ # 兼容旧版字段。
35
40
  QCLAW_STATE_DIR=C:\Users\Administrator\.qclaw
36
41
 
37
- # 【必填】QClaw 主配置文件路径。
38
- # = QCLAW_STATE_DIR 后面加 \qclaw.json,把 Administrator 改成你的用户名即可。
42
+ # 【可选】宿主 Runtime 主配置文件路径。
43
+ # QClaw 用户可继续填写 qclaw.json,其他宿主没有这个文件也不影响扫码连接。
39
44
  QCLAW_CONFIG_PATH=C:\Users\Administrator\.qclaw\qclaw.json
40
45
 
41
46
  # 【必填】OpenClaw(QClaw 内置 AI 框架)的配置文件路径。
@@ -53,9 +58,14 @@ OPENCLAW_CLI_PATH=D:\QClaw\resources\openclaw\node_modules\openclaw\openclaw.mjs
53
58
  # 以下为可选配置项,默认值通常够用,不需要改动
54
59
  # ──────────────────────────────────────────────────────────────────────────────
55
60
 
56
- # 【勿改】使用 QClaw 的哪个 Agent 执行任务,固定填 main(QClaw 主 Agent)。
61
+ # 【勿改】使用 OpenClaw 的哪个 Agent 执行任务,通常固定填 main
57
62
  OPENCLAW_AGENT_ID=main
58
63
 
64
+ # 【可选】如果你不是通过 QClaw 网关提供模型能力,也可以直接指定 OpenClaw 网关地址与鉴权。
65
+ OPENCLAW_GATEWAY_BASE_URL=
66
+ OPENCLAW_GATEWAY_API_KEY=
67
+ OPENCLAW_WECHAT_WS_URL=
68
+
59
69
  # 【可选】AI 执行一次任务的最长等待时间(毫秒)。
60
70
  # 默认 60000 = 60 秒。如果 AI 经常超时,可以调大,例如 120000(2 分钟)。
61
71
  OPENCLAW_COMMAND_TIMEOUT_MS=60000
package/README.md CHANGED
@@ -1,510 +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
- ## 一图看懂
51
+ ## 零密钥扫码流程
92
52
 
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. 到龙虾私聊页测试文本,再测试截图或文件能力。
53
+ 第一次运行时:
101
54
 
102
- ## 这套方案现在已经支持什么
55
+ - 当前目录没有 .env,程序会自动生成一份可选模板。
56
+ - 程序不会因为未填写 OPENCLAW_CONNECTOR_SECRET 而退出。
57
+ - 程序会直接创建扫码会话并显示二维码。
103
58
 
104
- 当前已经打通:
59
+ 扫码成功后:
105
60
 
106
- - 文本私聊回复。
107
- - 截图类任务回传图片。
108
- - 第三方 Runtime 按结构化附件格式回传图片或文件。
109
- - 心跳保活、扫码绑定、换账号接管 Runtime。
61
+ - 服务端会向这台电脑下发专用 connector token。
62
+ - token 会自动写入当前工作目录下的 .licity-connector/runtime.json。
63
+ - 后续心跳、拉任务、结果回写都会自动使用这个 token。
110
64
 
111
- 当前仍有限制:
65
+ 这意味着你不再需要从 APP 手动复制连接密钥,也不需要手动把密钥填进 .env。
112
66
 
113
- - 普通问答是否能真正执行,仍取决于你本机 QClaw/OpenClaw 环境是否可用。
114
- - 连接器默认通过 JSON 回传附件,建议单个附件控制在 8MB 以内。
115
- - 如果第三方程序只会回纯文本,不会输出结构化附件,那它就只能回文本,不能自动把本地文件带回里世界。
67
+ ## 最短使用步骤
116
68
 
117
- ## 适合谁用
69
+ ### 1. 先确认本机 Runtime 可用
118
70
 
119
- 适合以下场景:
120
-
121
- - 你已经在电脑上用 QClaw。
122
- - 你希望把本机 AI 能力映射成里世界里的龙虾。
123
- - 你想让龙虾能回复文本,或者进一步回传截图、图片、文件。
124
-
125
- 不适合以下场景:
126
-
127
- - 你手里已经是官方 OpenClaw Runtime,并且它本身就能直接生成里世界配对二维码。
128
- 这种情况优先走官方 OpenClaw 原生扫码,不需要这个桥接器。
129
-
130
- ## 先说结论
131
-
132
- 要让龙虾真正工作,至少要同时满足这 3 个条件:
133
-
134
- 1. QClaw 主程序已经启动,并保持登录和可用。
135
- 2. QClaw 启动后,本机本地网关已经监听,当前实际检查口径是 127.0.0.1:19000。
136
- 3. 本连接器已经启动,并成功和里世界中的某只龙虾完成扫码绑定。
71
+ 至少要满足:
137
72
 
138
- 如果第 1 或第 2 条不满足,APP 里给龙虾发消息后,任务虽然会进入队列,但 OpenClaw agent 无法真实执行。
73
+ - OpenClaw 配置文件存在。
74
+ - OpenClaw CLI 可执行。
75
+ - 你的宿主 Runtime 已启动,并且本地模型/网关可访问。
139
76
 
140
- ## 目录里有哪些文件
77
+ 如果你是 QClaw 用户,通常就是先启动 QClaw,并确认它的 OpenClaw 服务已经起来。
141
78
 
142
- - `index.js`:连接器主程序。
143
- - `.env.example`:环境变量模板。
144
- - `setup.js`:本地安装助手和自检脚本。
145
- - `data/runtime.json`:保存本机 Runtime ID。
146
- - `data/screenshots/`:截图任务产生的临时图片。
79
+ ### 2. 在工作目录运行连接器
147
80
 
148
- ## 新手最短路径
149
-
150
- ### 第 1 步:启动 QClaw 主程序
151
-
152
- QClaw 的位置取决于你的安装路径,常见的在:
153
- - Windows:`C:\QClaw\QClaw.exe` 或安装时指定的目录
154
- - 也可以通过桌面快捷方式直接启动
155
-
156
- 启动后不要立刻关闭,保持 QClaw 正常运行。
157
-
158
- ### 第 2 步:确认本地 19000 端口已监听
159
-
160
- 在 PowerShell 执行:
161
-
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
- ### .env 每个字段详细说明
247
-
248
- | 字段 | 必填/可选 | 默认值 / 说明 |
249
- |------|----------|--------------|
250
- | `LICITY_API_BASE_URL` | **勿改** | `https://li.city` — 服务器地址,固定 |
251
- | `OPENCLAW_CONNECTOR_SECRET` | **必填** | 连接密钥。里世界 APP → 我的龙虾 → 底部「复制连接密钥」按钮获取。未填则启动即报错 |
252
- | `CONNECTOR_PROVIDER` | **勿改** | `qclaw-local` — 连接器类型标识,固定 |
253
- | `DEVICE_NAME` | 可选 | 留空则自动用电脑主机名。多台电脑接入时用来区分,可填中文 |
254
- | `QCLAW_PATH` | **必填** | QClaw 主程序(.exe)完整路径。右键 QClaw 快捷方式 → 属性 → 「目标」里看到的路径 |
255
- | `QCLAW_STATE_DIR` | **必填** | QClaw 数据目录,存放配置和登录状态。⚠️ `Administrator` **必须改成你的 Windows 用户名**。查用户名:`Win+R` → `cmd` 回车 → 输入 `echo %USERNAME%` |
256
- | `QCLAW_CONFIG_PATH` | **必填** | `QCLAW_STATE_DIR` 的值 + `\qclaw.json`。例:`C:\Users\你的用户名\.qclaw\qclaw.json` |
257
- | `OPENCLAW_CONFIG_PATH` | **必填** | `QCLAW_STATE_DIR` 的值 + `\openclaw.json`。OpenClaw(QClaw 内置 AI 框架)配置文件,程序自动读取,无需手动编辑(除非你在配置 MCP 适配器) |
258
- | `OPENCLAW_CLI_PATH` | **必填** | OpenClaw 命令行程序路径,连接器通过它直接调用 AI 能力。路径格式固定:`QClaw安装目录\resources\openclaw\node_modules\openclaw\openclaw.mjs`,把开头改成你的 QClaw 安装目录即可 |
259
- | `OPENCLAW_AGENT_ID` | **勿改** | `main` — 使用 QClaw 的主 Agent,固定 |
260
- | `OPENCLAW_COMMAND_TIMEOUT_MS` | 可选 | AI 执行单次任务的最长等待时间(毫秒)。默认 `60000`(60 秒)。AI 经常超时时可调大,例如 `120000`(2 分钟) |
261
- | `CAPABILITY_SCOPES` | **勿改** | 连接器支持的功能域。`private_chat`=私信,`neighbor`=邻里圈,`anchor`=锚点,`time_travel`=穿越 |
262
- | `HEARTBEAT_INTERVAL_MS` | 可选 | 心跳信号发送间隔(毫秒)。默认 `25000`(25 秒)。用于维持龙虾「在线」状态,服务器超 3 分钟无心跳会标记离线 |
263
- | `POLL_INTERVAL_MS` | 可选 | 轮询新任务的频率(毫秒)。默认 `3000`(3 秒查一次)。值越小响应越快,但服务器请求也更多 |
264
-
265
- **最快速填写方法(Windows):**
266
-
267
- 1. 右键 QClaw 快捷方式 → 属性 → 复制「目标」里的路径 → 填入 `QCLAW_PATH`
268
- 2. 按 `Win+R` → 输入 `cmd` → `echo %USERNAME%` 查看用户名 → 替换 `Administrator`
269
- 3. `QCLAW_CONFIG_PATH` 和 `OPENCLAW_CONFIG_PATH` 直接复制 `QCLAW_STATE_DIR` 的值,末尾分别加 `\qclaw.json` 和 `\openclaw.json`
270
- 4. `OPENCLAW_CLI_PATH` 把 `QCLAW_PATH` 里的`.exe`文件名去掉(只保留目录),末尾加 `\resources\openclaw\node_modules\openclaw\openclaw.mjs`
271
-
272
- 如果 `OPENCLAW_CONNECTOR_SECRET` 没填或填错,程序会直接拒绝连接(提示"Connector 未授权")而不会启动。
93
+ ### 3. 用里世界 APP 扫码
273
94
 
274
- ### 第 5 步:扫码连接到里世界 APP
95
+ 路径:
275
96
 
276
- 当连接器启动成功后,终端会打印:
97
+ - 打开里世界 APP
98
+ - 进入“我的龙虾”
99
+ - 点击“扫码连接本地龙虾”
100
+ - 扫描终端二维码
101
+ - 选择一只龙虾并确认绑定
277
102
 
278
- - 当前 Runtime ID
279
- - QClaw 路径
280
- - OpenClaw 配置路径
281
- - 启动前检查结果
282
- - 一个二维码
103
+ ### 4. 验证连接
283
104
 
284
- 这时在里世界 APP 里操作:
105
+ 先发一句最短文本,例如:
285
106
 
286
- 1. 打开“我的龙虾”。
287
- 2. 找到“接入已有龙虾”。
288
- 3. 点击“扫码连接本地龙虾”。
289
- 4. 扫终端里的二维码。
290
- 5. 在确认页选择一只龙虾并确认。
107
+ - 请只回复:已收到
291
108
 
292
- 如果你当前账号下还没有龙虾,APP 会允许你先创建一只,再直接完成连接。
109
+ 文本正常后,再测试截图或文件能力。
293
110
 
294
- ### 6 步:验证链路
111
+ ## .env 说明
295
112
 
296
- 连接成功后,按下面顺序测:
113
+ .env 现在主要是为了补充本机路径和高级选项,不再是为了手填密钥。
297
114
 
298
- 1. 文本测试:`请只回复:已收到`
299
- 2. 普通问答:`帮我查北京明天的天气`
300
- 3. 截图测试:`帮我截图桌面`
301
- 4. 文件测试:让第三方程序返回结构化附件
115
+ ### 常用字段
302
116
 
303
- 建议永远先测短文本,再测截图,再测文件。这样最容易判断故障卡在哪一层。
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 | 功能域 |
304
129
 
305
- ## 连接器会输出哪些关键信息
130
+ ### 兼容旧字段
306
131
 
307
- ### 1. 启动前检查
132
+ 如果你以前已经写过以下字段,不需要重配,仍然兼容:
308
133
 
309
- 终端会输出类似:
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
310
141
 
311
- - OpenClaw 配置文件是否存在
312
- - OpenClaw CLI 是否存在
313
- - 模型网关地址
314
- - 19000 端口是否已监听
315
- - 微信通道地址
316
- - 网关探测是否成功
142
+ ## QClaw 用户说明
317
143
 
318
- ### 2. 二维码会话
144
+ 如果你使用的是 QClaw:
319
145
 
320
- 会输出类似:
146
+ - 可以继续沿用现有安装路径和 .qclaw 目录。
147
+ - 连接器会自动优先兼容 QClaw 的 wrapper、qclaw.json 和网关配置。
148
+ - 包名虽然还是 qclaw-local-connector,但现在推荐你把它理解成“OpenClaw 通用连接器”。
321
149
 
322
- - 新的连接会话 ID
323
- - 二维码原文 JSON
150
+ ## 其他 Runtime 用户说明
324
151
 
325
- ### 3. 心跳日志
152
+ 如果你不是 QClaw,而是其他兼容 OpenClaw CLI 的宿主:
326
153
 
327
- 会输出类似:
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
328
160
 
329
- `[Heartbeat 3] 龙虾=3号龙虾 权限快照已同步`
161
+ ## 当前能力
330
162
 
331
- 说明本地连接器和后端之间的保活链路是通的。
163
+ 已经打通:
332
164
 
333
- ### 4. 任务日志
165
+ - 扫码绑定
166
+ - 心跳保活
167
+ - 私聊文本回复
168
+ - 截图回传
169
+ - 图片 / 文件结构化回传
170
+ - 重新扫码接管旧连接
334
171
 
335
- 收到私聊任务时会输出:
172
+ ## 常见问题
336
173
 
337
- `[Task] 收到任务 private_chat_message (...)`
174
+ ### 1. 终端里出现二维码后,APP 扫码失败
338
175
 
339
- 如果进入普通问答,还会输出:
176
+ 先检查:
340
177
 
341
- `[Agent] 开始调用 OpenClaw,agent=main,宿主=QClaw.exe,入口=openclaw.mjs,直连=yes,wrapper=bypassed,超时=60000ms`
178
+ - 服务器地址是否能访问
179
+ - 终端二维码是否是最新生成的
180
+ - 是否有旧连接器实例占着同一个 runtimeId
342
181
 
343
- 如果最后出现:
344
-
345
- `[Task] 私聊回写成功`
346
-
347
- 说明至少“里世界 -> 连接器 -> 本地执行 -> 回写里世界”这条主链路已经走通。
348
-
349
- ## 如何判断问题卡在哪一层
350
-
351
- ### A. APP 能发消息,但连接器终端完全没有任务日志
352
-
353
- 这说明任务压根没有派发到本地连接器。
182
+ ### 2. 连接成功但龙虾不回复
354
183
 
355
184
  优先检查:
356
185
 
357
- - 这只龙虾是否真的显示已连接。
358
- - 是否扫的是当前这台连接器终端打印的二维码。
359
- - 是否连错了账号。
360
- - 是否刚切换账号但还没重新扫码接管 Runtime。
361
-
362
- ### B. 终端出现 `[Task] 收到任务`,但没有后续执行日志
363
-
364
- 说明里世界到连接器链路是通的。
365
-
366
- 问题在本机执行层,通常是:
367
-
368
- - OpenClaw 配置异常。
369
- - QClaw 主程序未完全就绪。
370
- - 本地网关虽然监听,但 agent 无法真正拿到模型结果。
371
-
372
- ### C. 19000 端口已监听,但启动前检查显示 `Access denied (PID)`
373
-
374
- 这代表一个很具体的现象:
375
-
376
- - 你的 QClaw 本地代理端口确实起来了。
377
- - 但外部普通 Node 进程直连探测这个代理时,被本机 PID 策略拒绝。
378
-
379
- 这不一定等于完全不能用。
380
-
381
- 如果后续任务执行日志里显示:
382
-
383
- - `直连=yes`
384
- - `wrapper=bypassed`
385
- - `私聊回写成功`
386
-
387
- 说明虽然 HTTP 探测被拦截,但当前连接器已经通过更可信的宿主路径完成了调用,实际聊天链路仍然可能是可用的。
388
-
389
- ### D. 文本能回,截图能回,但文件回不来
390
-
391
- 这通常不是里世界 APP 的问题,而是第三方程序本身没有输出结构化附件。
392
-
393
- 当前连接器支持两种附件输入:
394
-
395
- 1. 直接给出 `media_base64`
396
- 2. 给出本地文件路径,连接器读取后转为 base64 回传
397
-
398
- 如果第三方 Runtime 只输出一段纯文本,例如“文件已生成到 D:\xxx”,但没有在结构化结果里真正带出文件字段,连接器就没法自动把那个文件回传到里世界。
399
-
400
- ### E. 换账号扫码失败或旧账号仍占着连接
401
-
402
- 当前后端已经支持 Runtime 接管。
403
-
404
- 正常行为应该是:
405
-
406
- - 新账号扫码成功后,旧账号上的同一 Runtime 绑定自动失效。
407
- - 不需要手动删数据库或重装连接器。
408
-
409
- 如果仍失败,优先重新生成二维码再扫一次,并确认 APP 当前登录的是正确账号。
410
-
411
- ## 文件和图片回传的经验总结
412
-
413
- 这一部分是本次接第三方龙虾最关键的经验。
414
-
415
- ### 1. 里世界数据库本身支持媒体字段,不是后端存不下
186
+ - 本机 Runtime 是否真的启动
187
+ - OpenClaw CLI 是否可执行
188
+ - 本地模型/网关端口是否已监听
416
189
 
417
- 真正的短板往往不是数据库,而是“第三方 Runtime 有没有把媒体按规范回出来”。
190
+ ### 3. 以前必须填的 OPENCLAW_CONNECTOR_SECRET 现在还要填吗
418
191
 
419
- ### 2. 截图类任务最好在连接器侧直接做
192
+ 通常不需要。
420
193
 
421
- 例如“帮我截图桌面”这种需求,连接器可以直接本地截图,然后把图片回传给里世界。
194
+ 它现在只作为旧版兼容字段保留。正常扫码绑定流程下,连接器会自动拿到本机专用 token。
422
195
 
423
- 这样最稳定,因为:
196
+ ## 发布说明
424
197
 
425
- - 不依赖模型自己理解如何上传文件。
426
- - 不依赖第三方程序输出复杂附件格式。
427
-
428
- ### 3. 普通文件回传,关键在结构化附件
429
-
430
- 如果你要让第三方程序回传文件,建议它在结构化结果里带出至少这些字段之一:
431
-
432
- - `media_base64`
433
- - `media_file_path`
434
- - `file_base64`
435
- - `file_path`
436
-
437
- 以及这些辅助字段:
438
-
439
- - `media_name`
440
- - `media_mime_type`
441
- - `media_type`
442
-
443
- ### 4. 附件不要太大
444
-
445
- 当前连接器默认走 JSON 请求把附件回传到后端,建议单个附件控制在 8MB 以内。
446
-
447
- 如果你后续要走更大的文件,建议改成先上传对象存储,再只回传 URL。
448
-
449
- ## 推荐的标准操作顺序
450
-
451
- 1. 先启动 QClaw 主程序。
452
- 2. 用 `Get-NetTCPConnection` 确认 19000 已监听。
453
- 3. 进入本目录,执行 `npm install && npm run quickstart`。
454
- 4. 看安装助手是否通过关键检查。
455
- 5. 看连接器终端是否打印二维码。
456
- 6. 去 APP 里扫码绑定。
457
- 7. 先测短文本。
458
- 8. 再测截图。
459
- 9. 最后再测文件。
460
-
461
- ## 常用命令
462
-
463
- ### npm 全局安装用户
198
+ 当前推荐安装源仍然是:
464
199
 
465
200
  ```bash
466
- # 直接在 .env 文件所在目录运行
467
- licity-connector
468
- ```
469
-
470
- ### 本地克隆 / 下载用户
471
-
472
- ```bash
473
- # 一键准备并启动
474
- npm install && npm run quickstart
475
-
476
- # 只做环境自检
477
- npm run doctor
478
-
479
- # 只启动连接器
480
- npm run start
481
- ```
482
-
483
- ### 手工执行 OpenClaw 自检(Windows PowerShell)
484
-
485
- 把下面的路径替换成你实际的 QClaw 安装路径和状态目录:
486
-
487
- ```powershell
488
- $env:OPENCLAW_CONFIG_PATH="$env:USERPROFILE\.qclaw\openclaw.json"
489
- $env:QCLAW_LLM_BASE_URL='http://127.0.0.1:19000/proxy'
490
- $env:QCLAW_LLM_API_KEY='<从 openclaw.json 中获取的 apiKey>'
491
- $env:QCLAW_WECHAT_WS_URL='ws://127.0.0.1:19000/proxy'
492
- node '<QClaw安装目录>\resources\openclaw\node_modules\openclaw\openclaw.mjs' agent --agent main --message '请只回复四个字:已收到。不要解释。' --json --timeout 30
201
+ npm install -g @licity/qclaw-local-connector
493
202
  ```
494
203
 
495
- 如果这条命令都不能正常返回,问题就不在里世界,也不在这个连接器,而在你本机 QClaw/OpenClaw 环境本身。
496
-
497
- ## 最后的建议
498
-
499
- 如果你是小白,别一上来就测复杂问题。
500
-
501
- 永远按这个顺序:
502
-
503
- 1. 先看 19000 端口。
504
- 2. 再看连接器能不能出二维码。
505
- 3. 再看 APP 能不能扫码成功。
506
- 4. 再测短文本。
507
- 5. 再测截图。
508
- 6. 最后再测文件。
509
-
510
- 按这个顺序,你几乎总能很快定位问题卡在哪一层。
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.3",
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