@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 +71 -2
- package/README.md +119 -406
- package/index.js +142 -74
- package/package.json +7 -4
- package/setup.js +23 -12
- package//345/220/257/345/212/250/351/207/214/344/270/226/347/225/214OpenClaw/350/277/236/346/216/245/345/231/250.bat +31 -0
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
|
-
|
|
3
|
-
|
|
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
|
-
# 里世界
|
|
1
|
+
# 里世界 OpenClaw 本地连接器
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
这是里世界的本地龙虾连接器。它负责把你电脑上的 OpenClaw Runtime 接进里世界 APP,让某只龙虾真正映射到本机运行。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
它已经不再要求你先手填全局密钥。现在的流程是:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
1. 在电脑上运行一条命令。
|
|
8
|
+
2. 终端立即出现二维码。
|
|
9
|
+
3. 用里世界 APP 扫码并选择龙虾确认。
|
|
10
|
+
4. 连接器自动拿到本机专用令牌并保存到本地。
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
当前包名仍然保留为 @licity/qclaw-local-connector,是为了兼容旧用户;但从能力上,它已经是通用 OpenClaw 连接器,不再只限 QClaw。
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
## 适用范围
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
适合以下场景:
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
- 你使用 QClaw,且本机带有 OpenClaw CLI。
|
|
19
|
+
- 你使用其他兼容 OpenClaw CLI 的本地 Runtime。
|
|
20
|
+
- 你希望把本机 AI 能力映射成里世界里的龙虾,由 APP 给龙虾发消息,本机 Runtime 回复。
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
不适合以下场景:
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
- 你只是要做 MCP 直连工具调用,不需要把本机 Runtime 映射成龙虾。
|
|
25
|
+
- 你手里的官方 Runtime 已经内置里世界原生扫码绑定,并不需要桥接器。
|
|
22
26
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
第一次运行时:
|
|
133
54
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
55
|
+
- 当前目录没有 .env,程序会自动生成一份可选模板。
|
|
56
|
+
- 程序不会因为未填写 OPENCLAW_CONNECTOR_SECRET 而退出。
|
|
57
|
+
- 程序会直接创建扫码会话并显示二维码。
|
|
137
58
|
|
|
138
|
-
|
|
59
|
+
扫码成功后:
|
|
139
60
|
|
|
140
|
-
|
|
61
|
+
- 服务端会向这台电脑下发专用 connector token。
|
|
62
|
+
- token 会自动写入当前工作目录下的 .licity-connector/runtime.json。
|
|
63
|
+
- 后续心跳、拉任务、结果回写都会自动使用这个 token。
|
|
141
64
|
|
|
142
|
-
|
|
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
|
-
###
|
|
69
|
+
### 1. 先确认本机 Runtime 可用
|
|
151
70
|
|
|
152
|
-
|
|
153
|
-
- Windows:`C:\QClaw\QClaw.exe` 或安装时指定的目录
|
|
154
|
-
- 也可以通过桌面快捷方式直接启动
|
|
71
|
+
至少要满足:
|
|
155
72
|
|
|
156
|
-
|
|
73
|
+
- OpenClaw 配置文件存在。
|
|
74
|
+
- OpenClaw CLI 可执行。
|
|
75
|
+
- 你的宿主 Runtime 已启动,并且本地模型/网关可访问。
|
|
157
76
|
|
|
158
|
-
|
|
77
|
+
如果你是 QClaw 用户,通常就是先启动 QClaw,并确认它的 OpenClaw 服务已经起来。
|
|
159
78
|
|
|
160
|
-
|
|
79
|
+
### 2. 在工作目录运行连接器
|
|
161
80
|
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
260
|
-
-
|
|
261
|
-
-
|
|
262
|
-
-
|
|
263
|
-
-
|
|
97
|
+
- 打开里世界 APP
|
|
98
|
+
- 进入“我的龙虾”
|
|
99
|
+
- 点击“扫码连接本地龙虾”
|
|
100
|
+
- 扫描终端二维码
|
|
101
|
+
- 选择一只龙虾并确认绑定
|
|
264
102
|
|
|
265
|
-
|
|
103
|
+
### 4. 验证连接
|
|
266
104
|
|
|
267
|
-
|
|
268
|
-
2. 找到“接入已有龙虾”。
|
|
269
|
-
3. 点击“扫码连接本地龙虾”。
|
|
270
|
-
4. 扫终端里的二维码。
|
|
271
|
-
5. 在确认页选择一只龙虾并确认。
|
|
105
|
+
先发一句最短文本,例如:
|
|
272
106
|
|
|
273
|
-
|
|
107
|
+
- 请只回复:已收到
|
|
274
108
|
|
|
275
|
-
|
|
109
|
+
文本正常后,再测试截图或文件能力。
|
|
276
110
|
|
|
277
|
-
|
|
111
|
+
## .env 说明
|
|
278
112
|
|
|
279
|
-
|
|
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
|
-
###
|
|
130
|
+
### 兼容旧字段
|
|
289
131
|
|
|
290
|
-
|
|
132
|
+
如果你以前已经写过以下字段,不需要重配,仍然兼容:
|
|
291
133
|
|
|
292
|
-
-
|
|
293
|
-
-
|
|
294
|
-
-
|
|
295
|
-
-
|
|
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
|
-
|
|
142
|
+
## QClaw 用户说明
|
|
300
143
|
|
|
301
|
-
|
|
144
|
+
如果你使用的是 QClaw:
|
|
302
145
|
|
|
303
|
-
-
|
|
304
|
-
-
|
|
146
|
+
- 可以继续沿用现有安装路径和 .qclaw 目录。
|
|
147
|
+
- 连接器会自动优先兼容 QClaw 的 wrapper、qclaw.json 和网关配置。
|
|
148
|
+
- 包名虽然还是 qclaw-local-connector,但现在推荐你把它理解成“OpenClaw 通用连接器”。
|
|
305
149
|
|
|
306
|
-
|
|
150
|
+
## 其他 Runtime 用户说明
|
|
307
151
|
|
|
308
|
-
|
|
152
|
+
如果你不是 QClaw,而是其他兼容 OpenClaw CLI 的宿主:
|
|
309
153
|
|
|
310
|
-
|
|
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
|
-
|
|
163
|
+
已经打通:
|
|
315
164
|
|
|
316
|
-
|
|
165
|
+
- 扫码绑定
|
|
166
|
+
- 心跳保活
|
|
167
|
+
- 私聊文本回复
|
|
168
|
+
- 截图回传
|
|
169
|
+
- 图片 / 文件结构化回传
|
|
170
|
+
- 重新扫码接管旧连接
|
|
317
171
|
|
|
318
|
-
|
|
172
|
+
## 常见问题
|
|
319
173
|
|
|
320
|
-
|
|
174
|
+
### 1. 终端里出现二维码后,APP 扫码失败
|
|
321
175
|
|
|
322
|
-
|
|
176
|
+
先检查:
|
|
323
177
|
|
|
324
|
-
|
|
178
|
+
- 服务器地址是否能访问
|
|
179
|
+
- 终端二维码是否是最新生成的
|
|
180
|
+
- 是否有旧连接器实例占着同一个 runtimeId
|
|
325
181
|
|
|
326
|
-
|
|
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
|
-
###
|
|
190
|
+
### 3. 以前必须填的 OPENCLAW_CONNECTOR_SECRET 现在还要填吗
|
|
401
191
|
|
|
402
|
-
|
|
192
|
+
通常不需要。
|
|
403
193
|
|
|
404
|
-
|
|
194
|
+
它现在只作为旧版兼容字段保留。正常扫码绑定流程下,连接器会自动拿到本机专用 token。
|
|
405
195
|
|
|
406
|
-
|
|
407
|
-
- 不依赖第三方程序输出复杂附件格式。
|
|
196
|
+
## 发布说明
|
|
408
197
|
|
|
409
|
-
|
|
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
|
-
|
|
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 || '
|
|
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
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
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 (!
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
107
|
+
function readRuntimeState() {
|
|
93
108
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
94
109
|
|
|
95
110
|
if (fs.existsSync(runtimeFile)) {
|
|
96
111
|
try {
|
|
97
|
-
|
|
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
|
|
114
|
+
console.warn('读取本地 runtime.json 失败,将重新生成本地运行状态。');
|
|
103
115
|
}
|
|
104
116
|
}
|
|
105
117
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 =
|
|
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:
|
|
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(' 里世界
|
|
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(
|
|
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(
|
|
384
|
+
console.log(`! 检测到本地模型端口未启动。仅启动连接器不够,还需要先启动 ${runtimeDisplayName} 并保持其本地网关可用。`);
|
|
334
385
|
}
|
|
335
386
|
if (preflight.gatewayProbe?.message && String(preflight.gatewayProbe.message).includes('Access denied (PID)')) {
|
|
336
|
-
console.log(
|
|
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:
|
|
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:
|
|
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-
|
|
799
|
+
connector: 'licity-openclaw-local-connector',
|
|
746
800
|
nodeVersion: process.version,
|
|
747
|
-
|
|
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(
|
|
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-
|
|
788
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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('里世界
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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('
|
|
80
|
+
printCheck('Runtime 程序路径', fs.existsSync(runtimePath), runtimePath);
|
|
65
81
|
printCheck('OpenClaw 配置文件', fs.existsSync(openclawConfigPath), openclawConfigPath);
|
|
66
|
-
printCheck('连接器密钥',
|
|
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
|