@hupan56/wlkj 2.7.12 → 3.0.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/bin/cli.js +344 -78
- package/package.json +29 -29
- package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
- package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
- package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
- package/templates/.qoder/learning/eval-history.jsonl +28 -0
- package/templates/data/index/wiki-index.json +8 -0
- package/templates/qoder/agents/insight-planning.md +1 -1
- package/templates/qoder/agents/insight-research.md +28 -15
- package/templates/qoder/commands/optional/wl-insight.md +159 -161
- package/templates/qoder/commands/optional/wl-report.md +4 -4
- package/templates/qoder/commands/optional/wl-status.md +2 -2
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +2 -2
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +2 -2
- package/templates/qoder/commands/wl-req.md +43 -0
- package/templates/qoder/commands/wl-search.md +8 -8
- package/templates/qoder/commands/wl-task.md +17 -17
- package/templates/qoder/commands/wl-test.md +41 -15
- package/templates/qoder/config.yaml +17 -1
- package/templates/qoder/hooks/session-start.py +12 -22
- package/templates/qoder/nul +4 -0
- package/templates/qoder/rules/wl-pipeline.md +22 -48
- package/templates/qoder/scripts/README.md +139 -0
- package/templates/qoder/scripts/common/autotest_auth.py +109 -0
- package/templates/qoder/scripts/common/bootstrap.py +145 -0
- package/templates/qoder/scripts/common/check_publish.py +98 -0
- package/templates/qoder/scripts/common/cmd_registry.py +112 -0
- package/templates/qoder/scripts/common/config.py +187 -0
- package/templates/qoder/scripts/common/contract.py +317 -0
- package/templates/qoder/scripts/common/developer.py +2 -1
- package/templates/qoder/scripts/common/feishu.py +10 -9
- package/templates/qoder/scripts/common/guard.py +159 -0
- package/templates/qoder/scripts/common/identity.py +121 -2
- package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
- package/templates/qoder/scripts/common/mcp_base.py +268 -0
- package/templates/qoder/scripts/common/paths.py +187 -1
- package/templates/qoder/scripts/common/result.py +223 -0
- package/templates/qoder/scripts/common/roles.py +60 -0
- package/templates/qoder/scripts/common/task_utils.py +21 -9
- package/templates/qoder/scripts/common/test_extract.py +115 -0
- package/templates/qoder/scripts/kg/__init__.py +11 -0
- package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
- package/templates/qoder/scripts/kg/build_relations.py +127 -0
- package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
- package/templates/qoder/scripts/kg/build_workflows.py +144 -0
- package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
- package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
- package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
- package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
- package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
- package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
- package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
- package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
- package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
- package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
- package/templates/qoder/scripts/kg/prefetch.py +359 -0
- package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
- package/templates/qoder/scripts/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
- package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
- package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
- package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
- package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
- package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
- package/templates/qoder/scripts/report/__init__.py +11 -0
- package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
- package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
- package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
- package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
- package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
- package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
- package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
- package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
- package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
- package/templates/qoder/scripts/report/req.py +222 -0
- package/templates/qoder/scripts/report/role.py +33 -0
- package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
- package/templates/qoder/scripts/setup/__init__.py +11 -0
- package/templates/qoder/scripts/setup/carriers.py +662 -0
- package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
- package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
- package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
- package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
- package/templates/qoder/scripts/{setup.py → setup/setup.py} +113 -4
- package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
- package/templates/qoder/scripts/task/__init__.py +11 -0
- package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
- package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
- package/templates/qoder/scripts/task/task.py +221 -0
- package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
- package/templates/qoder/scripts/task/task_query.py +161 -0
- package/templates/qoder/scripts/task/task_relations.py +424 -0
- package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
- package/templates/qoder/scripts/test/__init__.py +11 -0
- package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
- package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
- package/templates/qoder/scripts/test/autotest_data.py +675 -0
- package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
- package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
- package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
- package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
- package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
- package/templates/qoder/scripts/wlkj.py +116 -0
- package/templates/qoder/settings.json +1 -10
- package/templates/qoder/skills/design-import/SKILL.md +226 -226
- package/templates/qoder/skills/design-review/SKILL.md +82 -82
- package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
- package/templates/qoder/skills/prd-review/SKILL.md +5 -5
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
- package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
- package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
- package/templates/qoder/skills/test-generator/SKILL.md +5 -5
- package/templates/qoder/skills/wl-code/SKILL.md +4 -4
- package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
- package/templates/qoder/skills/wl-design/SKILL.md +3 -3
- package/templates/qoder/skills/wl-init/SKILL.md +8 -8
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
- package/templates/qoder/skills/wl-report/SKILL.md +7 -7
- package/templates/qoder/skills/wl-search/SKILL.md +13 -13
- package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
- package/templates/qoder/skills/wl-status/SKILL.md +5 -5
- package/templates/qoder/skills/wl-task/SKILL.md +6 -6
- package/templates/qoder/skills/wl-test/SKILL.md +102 -39
- package/templates/root/AGENTS.md +39 -40
- package/templates/qoder/hooks/inject-workflow-state.py +0 -169
- package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/check_carriers.py +0 -238
- package/templates/qoder/scripts/check_mcp_launch.py +0 -183
- package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
- package/templates/qoder/scripts/collect_prds.py +0 -31
- package/templates/qoder/scripts/common/mentions.py +0 -134
- package/templates/qoder/scripts/common/utf8.py +0 -38
- package/templates/qoder/scripts/extract_routes.py +0 -54
- package/templates/qoder/scripts/extract_routes_tree.py +0 -78
- package/templates/qoder/scripts/handoff.py +0 -22
- package/templates/qoder/scripts/init_developer.py +0 -76
- package/templates/qoder/scripts/parse_prds.py +0 -33
- package/templates/qoder/scripts/role.py +0 -51
- package/templates/qoder/scripts/sync_carriers.py +0 -259
- package/templates/qoder/scripts/task.py +0 -1261
- package/templates/qoder/scripts/workspace_init.py +0 -102
- package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
|
@@ -1,963 +1,973 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
python .qoder/scripts/setup_lanhu.py --
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def
|
|
143
|
-
"""
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if
|
|
232
|
-
|
|
233
|
-
return ('
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
#
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
warn('
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
#
|
|
342
|
-
|
|
343
|
-
#
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
cookie
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
print(
|
|
430
|
-
print()
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
#
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
existing =
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
print('
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
print('
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
return
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
#
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
report['
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
if
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
print('
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
print('
|
|
958
|
-
print('
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
4
|
+
import os as _o, sys as _s
|
|
5
|
+
_cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
|
|
6
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
7
|
+
from bootstrap import setup; setup()
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
setup_lanhu.py - 蓝湖 MCP 引导器 (幂等)
|
|
11
|
+
|
|
12
|
+
把第三方 dsphper/lanhu-mcp 引导成本机可用: clone + venv 依赖 + playwright + cookie + mcp.json 注册。
|
|
13
|
+
|
|
14
|
+
蓝湖 MCP 与我们自己的 kg_mcp_server.py 不同:
|
|
15
|
+
- 它是 HTTP server (http://localhost:8000/mcp), 需要独立常驻启动
|
|
16
|
+
(不像 kg_mcp_server.py 被 QoderWork 通过 command/args 自动拉起)
|
|
17
|
+
- cookie (LANHU_COOKIE) 在它的 .env 里, 必须用户登录 lanhuapp.com 后
|
|
18
|
+
从浏览器 DevTools 复制 (AI 拿不到)
|
|
19
|
+
- QoderWork 的 mcp.json 用 url 字段注册 (Streamable HTTP), 不是 command/args
|
|
20
|
+
|
|
21
|
+
被谁调用:
|
|
22
|
+
- setup.py Step 4.5: 主初始化流程
|
|
23
|
+
- install_qoderwork.py: 技能安装时同步注册 mcp.json
|
|
24
|
+
- 用户手动: python .qoder/scripts/setup_lanhu.py [--check|--uninstall]
|
|
25
|
+
|
|
26
|
+
用法:
|
|
27
|
+
python .qoder/scripts/setup_lanhu.py # 引导 + 注册 (幂等)
|
|
28
|
+
python .qoder/scripts/setup_lanhu.py --check # 只体检不改动
|
|
29
|
+
python .qoder/scripts/setup_lanhu.py --uninstall # 删 external/lanhu-mcp/ + mcp.json 条目
|
|
30
|
+
python .qoder/scripts/setup_lanhu.py --skip-install # 只注册 mcp.json, 不 clone/装依赖
|
|
31
|
+
|
|
32
|
+
幂等: 已 clone 跳过 clone; venv+依赖装过跳过; cookie 有效跳过; mcp.json 有 lanhu 跳过。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import shutil
|
|
39
|
+
import socket
|
|
40
|
+
import subprocess
|
|
41
|
+
import sys
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
46
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
47
|
+
except (AttributeError, TypeError, OSError):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
THIS_DIR = Path(__file__).resolve().parent
|
|
51
|
+
BASE = THIS_DIR.parent.parent.parent # repo root (M2子目录化: setup/ → scripts/ → .qoder/ → repo)
|
|
52
|
+
|
|
53
|
+
LANHU_REPO = 'https://github.com/dsphper/lanhu-mcp.git'
|
|
54
|
+
EXTERNAL_DIR = BASE / 'external'
|
|
55
|
+
LANHU_DIR = EXTERNAL_DIR / 'lanhu-mcp'
|
|
56
|
+
LANHU_VENV = LANHU_DIR / '.venv'
|
|
57
|
+
LANHU_SERVER_SCRIPT = LANHU_DIR / 'lanhu_mcp_server.py'
|
|
58
|
+
COOKIE_PLACEHOLDER = 'PUT_YOUR_COOKIE_HERE'
|
|
59
|
+
LANHU_PORT = 8000
|
|
60
|
+
LANHU_MCP_PATH = '/mcp'
|
|
61
|
+
|
|
62
|
+
# QoderWork mcp.json 位置
|
|
63
|
+
if sys.platform == 'win32':
|
|
64
|
+
_HOME = Path(os.environ.get('USERPROFILE', str(Path.home())))
|
|
65
|
+
else:
|
|
66
|
+
_HOME = Path.home()
|
|
67
|
+
QODERWORK_MCP_FILE = _HOME / '.qoderwork' / 'mcp.json'
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _current_developer():
|
|
71
|
+
"""读 .qoder/.developer 确定当前用户。没有返回 None。"""
|
|
72
|
+
try:
|
|
73
|
+
sys.path.insert(0, str(THIS_DIR))
|
|
74
|
+
from common.paths import get_developer
|
|
75
|
+
return get_developer(BASE)
|
|
76
|
+
except Exception:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def lanhu_secrets_dir(developer=None):
|
|
81
|
+
"""当前用户的私密信息目录 (v3.0: 收进 .private/secrets/, 向后兼容老 .secrets/)。
|
|
82
|
+
|
|
83
|
+
按 .developer 隔离: 一台机器多人切换时, 各自的 cookie 互不覆盖。
|
|
84
|
+
"""
|
|
85
|
+
dev = developer or _current_developer() or 'default'
|
|
86
|
+
try:
|
|
87
|
+
sys.path.insert(0, str(BASE / '.qoder' / 'scripts'))
|
|
88
|
+
from common.paths import get_secrets_dir
|
|
89
|
+
return Path(get_secrets_dir(dev))
|
|
90
|
+
except Exception:
|
|
91
|
+
return BASE / 'workspace' / 'members' / dev / '.secrets'
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def lanhu_env_file(developer=None):
|
|
95
|
+
"""当前用户的蓝湖 cookie 文件: workspace/members/{dev}/.secrets/lanhu.env"""
|
|
96
|
+
return lanhu_secrets_dir(developer) / 'lanhu.env'
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ============================================================
|
|
100
|
+
# 工具函数
|
|
101
|
+
# ============================================================
|
|
102
|
+
|
|
103
|
+
def ok(msg):
|
|
104
|
+
print(f' [\u2713] {msg}') # ✓
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def warn(msg):
|
|
108
|
+
print(f' [!] {msg}')
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def fail(msg):
|
|
112
|
+
print(f' [\u2717] {msg}', file=sys.stderr)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def ask(prompt, default='y'):
|
|
116
|
+
"""交互式 yes/no, 默认 default。非交互环境返回 default。"""
|
|
117
|
+
if not sys.stdin.isatty():
|
|
118
|
+
return default.lower() in ('y', 'yes')
|
|
119
|
+
try:
|
|
120
|
+
ans = input(f'{prompt} [{default}] ').strip().lower() or default
|
|
121
|
+
return ans in ('y', 'yes')
|
|
122
|
+
except (EOFError, KeyboardInterrupt):
|
|
123
|
+
return default.lower() in ('y', 'yes')
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def run(cmd, **kwargs):
|
|
127
|
+
"""跑子进程, 返回 CompletedProcess。encoding 容错。"""
|
|
128
|
+
kwargs.setdefault('capture_output', True)
|
|
129
|
+
kwargs.setdefault('text', True)
|
|
130
|
+
kwargs.setdefault('encoding', 'utf-8')
|
|
131
|
+
kwargs.setdefault('errors', 'replace')
|
|
132
|
+
return subprocess.run(cmd, **kwargs)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _venv_python(venv: Path) -> Path:
|
|
136
|
+
"""venv 内的 python 可执行路径 (跨平台)。"""
|
|
137
|
+
if sys.platform == 'win32':
|
|
138
|
+
return venv / 'Scripts' / 'python.exe'
|
|
139
|
+
return venv / 'bin' / 'python'
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _venv_pip(venv: Path) -> list:
|
|
143
|
+
"""venv 内的 pip 调用命令 (用 python -m pip, 更稳)。"""
|
|
144
|
+
return [str(_venv_python(venv)), '-m', 'pip']
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# lanhu-mcp 的 fastmcp 依赖需要 Python >= 3.10。
|
|
148
|
+
# 不能盲用 sys.executable (本机可能是 3.9), 得找一个 3.10+ 的解释器建 venv。
|
|
149
|
+
LANHU_MIN_PYTHON = (3, 10)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _python_version(exe):
|
|
153
|
+
"""返回解释器的 (major, minor) 元组, 探测失败返回 None。"""
|
|
154
|
+
try:
|
|
155
|
+
r = run([exe, '-c', 'import sys; print(sys.version_info[:2])'])
|
|
156
|
+
if r.returncode == 0:
|
|
157
|
+
import ast
|
|
158
|
+
ver = ast.literal_eval(r.stdout.strip())
|
|
159
|
+
if isinstance(ver, tuple) and len(ver) >= 2:
|
|
160
|
+
return (ver[0], ver[1])
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def find_python_310():
|
|
167
|
+
"""找一个 Python >= 3.10 的解释器路径, 优先级:
|
|
168
|
+
1. sys.executable (如果本身就 >= 3.10, 直接用, 最省事)
|
|
169
|
+
2. Windows py launcher: py -3.10 / -3.11 / -3.12 / -3.13
|
|
170
|
+
3. 常见命令: python3.10/3.11/3.12, python3
|
|
171
|
+
4. 兜底返回 sys.executable (让 pip 报错给用户看真原因)
|
|
172
|
+
"""
|
|
173
|
+
# 1. 当前解释器够新
|
|
174
|
+
if sys.version_info[:2] >= LANHU_MIN_PYTHON:
|
|
175
|
+
return sys.executable
|
|
176
|
+
|
|
177
|
+
# 2. Windows py launcher
|
|
178
|
+
if sys.platform == 'win32':
|
|
179
|
+
for v in ('3.13', '3.12', '3.11', '3.10'):
|
|
180
|
+
launcher = 'py' if shutil.which('py') else ('py.exe' if shutil.which('py.exe') else None)
|
|
181
|
+
if not launcher:
|
|
182
|
+
break
|
|
183
|
+
r = run([launcher, f'-{v}', '-c', 'import sys; print(sys.executable)'])
|
|
184
|
+
if r.returncode == 0 and r.stdout.strip():
|
|
185
|
+
exe = r.stdout.strip().strip('"')
|
|
186
|
+
ver = _python_version(exe)
|
|
187
|
+
if ver and ver >= LANHU_MIN_PYTHON:
|
|
188
|
+
return exe
|
|
189
|
+
|
|
190
|
+
# 3. 常见命令名 (跨平台)
|
|
191
|
+
for cand in ('python3.13', 'python3.12', 'python3.11', 'python3.10',
|
|
192
|
+
'python3', 'python'):
|
|
193
|
+
path = shutil.which(cand)
|
|
194
|
+
if path:
|
|
195
|
+
ver = _python_version(path)
|
|
196
|
+
if ver and ver >= LANHU_MIN_PYTHON:
|
|
197
|
+
return path
|
|
198
|
+
|
|
199
|
+
# 4. 兜底
|
|
200
|
+
return sys.executable
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ============================================================
|
|
204
|
+
# Step 1: clone / update lanhu-mcp
|
|
205
|
+
# ============================================================
|
|
206
|
+
|
|
207
|
+
def ensure_clone(dry=False, interactive=True):
|
|
208
|
+
"""clone lanhu-mcp 到 external/lanhu-mcp/, 已存在则 git pull 更新。
|
|
209
|
+
|
|
210
|
+
返回 (status, path): status in ('cloned', 'updated', 'ok', 'failed', 'would', 'absent')
|
|
211
|
+
"""
|
|
212
|
+
print('\n--- 1. 蓝湖 MCP 源码 ---')
|
|
213
|
+
if not shutil.which('git'):
|
|
214
|
+
warn('git 未安装, 无法 clone lanhu-mcp (装 git 后重跑)')
|
|
215
|
+
return ('failed', None)
|
|
216
|
+
|
|
217
|
+
if not LANHU_DIR.is_dir():
|
|
218
|
+
if dry:
|
|
219
|
+
print(f' [MISS] 未 clone: {LANHU_DIR}')
|
|
220
|
+
return ('would', None)
|
|
221
|
+
EXTERNAL_DIR.mkdir(parents=True, exist_ok=True)
|
|
222
|
+
print(f' git clone {LANHU_REPO} -> {LANHU_DIR}')
|
|
223
|
+
r = run(['git', 'clone', '--depth', '1', LANHU_REPO, str(LANHU_DIR)])
|
|
224
|
+
if r.returncode != 0:
|
|
225
|
+
fail('clone 失败: ' + (r.stderr or '').strip()[:200])
|
|
226
|
+
return ('failed', None)
|
|
227
|
+
ok(f'已 clone: {LANHU_DIR}')
|
|
228
|
+
return ('cloned', LANHU_DIR)
|
|
229
|
+
|
|
230
|
+
# 已存在: 检查是否是有效 repo
|
|
231
|
+
if not (LANHU_DIR / '.git').is_dir():
|
|
232
|
+
warn(f'{LANHU_DIR} 存在但不是 git 仓库, 跳过更新 (手动处理)')
|
|
233
|
+
return ('ok', LANHU_DIR)
|
|
234
|
+
|
|
235
|
+
if dry:
|
|
236
|
+
ok(f'已存在: {LANHU_DIR}')
|
|
237
|
+
return ('ok', LANHU_DIR)
|
|
238
|
+
|
|
239
|
+
# git pull 更新 (best-effort, 失败不阻塞)
|
|
240
|
+
r = run(['git', '-C', str(LANHU_DIR), 'pull', '--ff-only'])
|
|
241
|
+
if r.returncode == 0:
|
|
242
|
+
ok(f'已更新: {LANHU_DIR}')
|
|
243
|
+
return ('updated', LANHU_DIR)
|
|
244
|
+
warn('git pull 失败 (本地可能有改动), 用现有版本继续: '
|
|
245
|
+
+ (r.stderr or '').strip()[:120])
|
|
246
|
+
return ('ok', LANHU_DIR)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ============================================================
|
|
250
|
+
# Step 2: venv + 依赖 + playwright chromium
|
|
251
|
+
# ============================================================
|
|
252
|
+
|
|
253
|
+
def ensure_deps(dry=False, _rebuilt=False):
|
|
254
|
+
"""建隔离 venv, 装 requirements.txt + playwright chromium。
|
|
255
|
+
|
|
256
|
+
幂等: venv+依赖装过则跳过 (靠一个 marker 文件判断)。
|
|
257
|
+
_rebuilt: 内部防无限递归 (删旧 venv 重建只允许一次)。
|
|
258
|
+
"""
|
|
259
|
+
print('\n--- 2. 蓝湖 MCP 依赖 ---')
|
|
260
|
+
if not LANHU_DIR.is_dir():
|
|
261
|
+
warn('lanhu-mcp 未 clone, 跳过依赖安装')
|
|
262
|
+
return 'absent'
|
|
263
|
+
|
|
264
|
+
marker = LANHU_DIR / '.deps-installed'
|
|
265
|
+
req_file = LANHU_DIR / 'requirements.txt'
|
|
266
|
+
|
|
267
|
+
# 已装过 (marker 在 + requirements 没变) → 跳过
|
|
268
|
+
if marker.is_file() and req_file.is_file():
|
|
269
|
+
try:
|
|
270
|
+
marker_mtime = marker.stat().st_mtime
|
|
271
|
+
req_mtime = req_file.stat().st_mtime
|
|
272
|
+
if marker_mtime > req_mtime:
|
|
273
|
+
ok(f'依赖已装 (venv: {LANHU_VENV})')
|
|
274
|
+
return 'ok'
|
|
275
|
+
except OSError:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
if dry:
|
|
279
|
+
print(f' [MISS] 依赖未装: {LANHU_VENV}')
|
|
280
|
+
return 'would'
|
|
281
|
+
|
|
282
|
+
# 建 venv (用 3.10+ Python, fastmcp 要求)
|
|
283
|
+
base_python = find_python_310()
|
|
284
|
+
base_ver = _python_version(base_python)
|
|
285
|
+
if not LANHU_VENV.is_dir():
|
|
286
|
+
if base_ver and base_ver >= LANHU_MIN_PYTHON:
|
|
287
|
+
print(f' 建 venv (Python {base_ver[0]}.{base_ver[1]}): '
|
|
288
|
+
f'"{base_python}" -m venv {LANHU_VENV}')
|
|
289
|
+
else:
|
|
290
|
+
# 找不到 3.10+, 仍尝试建 (pip 会报错, 但给用户明确指引)
|
|
291
|
+
warn(f'未找到 Python >= 3.10 (fastmcp 要求)。当前最佳: '
|
|
292
|
+
f'{base_python} ({base_ver})')
|
|
293
|
+
print(f' 建 venv: "{base_python}" -m venv {LANHU_VENV}')
|
|
294
|
+
print(' ⚠ 预计 pip install 会失败 — 装 Python 3.10+ 后重跑')
|
|
295
|
+
r = run([base_python, '-m', 'venv', str(LANHU_VENV)])
|
|
296
|
+
if r.returncode != 0:
|
|
297
|
+
fail('venv 创建失败: ' + (r.stderr or '').strip()[:200])
|
|
298
|
+
return 'failed'
|
|
299
|
+
elif base_ver and base_ver < LANHU_MIN_PYTHON and not _rebuilt:
|
|
300
|
+
# venv 已存在但可能是旧 3.9 建的 (之前版本的 bug), 删了重建。
|
|
301
|
+
# _rebuilt 防无限递归: 若本机没有 3.10+, 删了重建还是 3.9, 不再删第二次。
|
|
302
|
+
venv_ver = _python_version(_venv_python(LANHU_VENV))
|
|
303
|
+
if venv_ver and venv_ver < LANHU_MIN_PYTHON:
|
|
304
|
+
warn(f'现有 venv 是 Python {venv_ver[0]}.{venv_ver[1]} (fastmcp 需 3.10+), '
|
|
305
|
+
f'删了重建')
|
|
306
|
+
try:
|
|
307
|
+
shutil.rmtree(LANHU_VENV)
|
|
308
|
+
except OSError:
|
|
309
|
+
pass
|
|
310
|
+
return ensure_deps(dry=dry, _rebuilt=True)
|
|
311
|
+
|
|
312
|
+
py = _venv_python(LANHU_VENV)
|
|
313
|
+
if not py.is_file():
|
|
314
|
+
fail(f'venv python 不存在: {py}')
|
|
315
|
+
return 'failed'
|
|
316
|
+
|
|
317
|
+
# 升级 pip
|
|
318
|
+
print(' 升级 pip...')
|
|
319
|
+
run(_venv_pip(LANHU_VENV) + ['install', '--upgrade', 'pip'])
|
|
320
|
+
|
|
321
|
+
# 装 requirements.txt
|
|
322
|
+
if req_file.is_file():
|
|
323
|
+
print(f' pip install -r requirements.txt')
|
|
324
|
+
r = run(_venv_pip(LANHU_VENV) + ['install', '-r', str(req_file)])
|
|
325
|
+
if r.returncode != 0:
|
|
326
|
+
fail('pip install requirements 失败: ' + (r.stderr or '').strip()[:200])
|
|
327
|
+
return 'failed'
|
|
328
|
+
ok('依赖已装')
|
|
329
|
+
else:
|
|
330
|
+
warn('requirements.txt 不存在, 跳过 (lanhu-mcp 目录结构可能变了)')
|
|
331
|
+
|
|
332
|
+
# playwright install chromium (150-300MB, 一次性)
|
|
333
|
+
print(' playwright install chromium (一次性, 约 150-300MB)...')
|
|
334
|
+
r = run([str(py), '-m', 'playwright', 'install', 'chromium'])
|
|
335
|
+
if r.returncode == 0:
|
|
336
|
+
ok('chromium 已装')
|
|
337
|
+
else:
|
|
338
|
+
warn('playwright install chromium 失败 (可稍后手动跑: '
|
|
339
|
+
f'{py} -m playwright install chromium)')
|
|
340
|
+
warn(' 原因: ' + (r.stderr or '').strip()[:120])
|
|
341
|
+
# 不阻塞: cookie/服务启动后 playwright 缺失会在用时才报错
|
|
342
|
+
|
|
343
|
+
# 写 marker
|
|
344
|
+
try:
|
|
345
|
+
marker.write_text('deps installed\n', encoding='utf-8')
|
|
346
|
+
except OSError:
|
|
347
|
+
pass
|
|
348
|
+
return 'ok'
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ============================================================
|
|
352
|
+
# Step 3: cookie (按开发者隔离, workspace/members/{dev}/.secrets/lanhu.env)
|
|
353
|
+
# ============================================================
|
|
354
|
+
# 为什么不放 external/lanhu-mcp/.env:
|
|
355
|
+
# 1. cookie 是个人登录凭证, 不该进仓库代码区 (即使 gitignore)
|
|
356
|
+
# 2. 同一台机器多人切换 .developer 时, 单一 .env 会互相覆盖
|
|
357
|
+
# 3. 跟 .signing_key / autotest-data.yaml 同一套隔离机制, 一致
|
|
358
|
+
# 蓝湖服务端怎么读到这个 cookie:
|
|
359
|
+
# lanhu_mcp_server.py 用 load_dotenv(override=False), 已有环境变量优先。
|
|
360
|
+
# 所以启动时把 LANHU_COOKIE 作为环境变量注入即可 (见 start_lanhu_server)。
|
|
361
|
+
|
|
362
|
+
def _read_env_cookie(developer=None):
|
|
363
|
+
"""读当前用户 lanhu.env 里的 LANHU_COOKIE, 没有返回 None。"""
|
|
364
|
+
env_file = lanhu_env_file(developer)
|
|
365
|
+
if not env_file.is_file():
|
|
366
|
+
return None
|
|
367
|
+
try:
|
|
368
|
+
for line in env_file.read_text(encoding='utf-8', errors='replace').splitlines():
|
|
369
|
+
line = line.strip()
|
|
370
|
+
if line.startswith('LANHU_COOKIE') and '=' in line:
|
|
371
|
+
val = line.split('=', 1)[1].strip().strip('"').strip("'")
|
|
372
|
+
return val
|
|
373
|
+
except OSError:
|
|
374
|
+
pass
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _write_env_cookie(cookie, developer=None):
|
|
379
|
+
"""把 LANHU_COOKIE 写进当前用户的 lanhu.env (保留其他行)。
|
|
380
|
+
|
|
381
|
+
文件: workspace/members/{dev}/.secrets/lanhu.env (已 gitignore)
|
|
382
|
+
"""
|
|
383
|
+
env_file = lanhu_env_file(developer)
|
|
384
|
+
try:
|
|
385
|
+
env_file.parent.mkdir(parents=True, exist_ok=True)
|
|
386
|
+
lines = []
|
|
387
|
+
found = False
|
|
388
|
+
if env_file.is_file():
|
|
389
|
+
for line in env_file.read_text(encoding='utf-8', errors='replace').splitlines():
|
|
390
|
+
if line.strip().startswith('LANHU_COOKIE') and '=' in line:
|
|
391
|
+
lines.append(f'LANHU_COOKIE="{cookie}"')
|
|
392
|
+
found = True
|
|
393
|
+
else:
|
|
394
|
+
lines.append(line)
|
|
395
|
+
if not found:
|
|
396
|
+
lines.append(f'LANHU_COOKIE="{cookie}"')
|
|
397
|
+
env_file.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
|
398
|
+
return True
|
|
399
|
+
except OSError as e:
|
|
400
|
+
fail(f'写 {env_file} 失败: {e}')
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def ensure_cookie(dry=False):
|
|
405
|
+
"""收 cookie 写 .env。已有真 cookie 跳过; 没有则交互式引导。
|
|
406
|
+
|
|
407
|
+
返回 status: 'ok' (有真 cookie) / 'placeholder' (占位) / 'would' / 'absent'
|
|
408
|
+
"""
|
|
409
|
+
dev = _current_developer()
|
|
410
|
+
env_file = lanhu_env_file(dev)
|
|
411
|
+
print('\n--- 3. 蓝湖 Cookie ---')
|
|
412
|
+
if dev:
|
|
413
|
+
print(f' 当前开发者: {dev}')
|
|
414
|
+
print(f' cookie 文件: {env_file} (按人隔离, 不进 git)')
|
|
415
|
+
cookie = _read_env_cookie(dev)
|
|
416
|
+
if cookie and cookie != COOKIE_PLACEHOLDER:
|
|
417
|
+
ok(f'蓝湖 cookie 已配置 ({env_file.name})')
|
|
418
|
+
return 'ok'
|
|
419
|
+
|
|
420
|
+
if dry:
|
|
421
|
+
if cookie == COOKIE_PLACEHOLDER:
|
|
422
|
+
print(f' [WARN] {env_file.name} 里是占位 cookie, 需要换成真的')
|
|
423
|
+
else:
|
|
424
|
+
print(f' [MISS] 没有 LANHU_COOKIE ({env_file})')
|
|
425
|
+
return 'would'
|
|
426
|
+
|
|
427
|
+
# 交互式引导
|
|
428
|
+
if sys.stdin.isatty():
|
|
429
|
+
print()
|
|
430
|
+
print(' 蓝湖 cookie 获取步骤:')
|
|
431
|
+
print(' 1. 浏览器打开 https://lanhuapp.com 并登录')
|
|
432
|
+
print(' 2. 按 F12 打开开发者工具, 切到 Network 标签')
|
|
433
|
+
print(' 3. 刷新页面 (F5), 点任意一个请求')
|
|
434
|
+
print(' 4. Request Headers 里找 Cookie: 这一行, 复制【整行】')
|
|
435
|
+
print(' (或 Application → Cookies → lanhuapp.com, 把所有项拼成')
|
|
436
|
+
print(' name1=value1; name2=value2 格式)')
|
|
437
|
+
print(' ⚠ 必须【整行】, 不能只复制 user_token 一个!')
|
|
438
|
+
print(' 实测: 只给 user_token 蓝湖 API 返回 418 (鉴权失败),')
|
|
439
|
+
print(' 需要 user_token + session + 其他项的组合才认。')
|
|
440
|
+
print()
|
|
441
|
+
try:
|
|
442
|
+
cookie = input(' 粘贴蓝湖 cookie (留空跳过, 之后编辑 lanhu.env): ').strip()
|
|
443
|
+
except (EOFError, KeyboardInterrupt):
|
|
444
|
+
cookie = ''
|
|
445
|
+
if cookie and len(cookie) > 20:
|
|
446
|
+
if _write_env_cookie(cookie, dev):
|
|
447
|
+
ok(f'蓝湖 cookie 已写入 {env_file}')
|
|
448
|
+
return 'ok'
|
|
449
|
+
return 'placeholder'
|
|
450
|
+
warn(f'未输入 cookie, 写入占位 (之后编辑 {env_file} 换成真的)')
|
|
451
|
+
else:
|
|
452
|
+
# 非交互环境 (AI 调用): 写占位
|
|
453
|
+
warn('非交互环境, 无法收 cookie, 写入占位')
|
|
454
|
+
|
|
455
|
+
if _write_env_cookie(COOKIE_PLACEHOLDER, dev):
|
|
456
|
+
print(f' 占位已写入: {env_file}')
|
|
457
|
+
print(f' 编辑该文件, 把 {COOKIE_PLACEHOLDER} 换成真 cookie')
|
|
458
|
+
return 'placeholder'
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# ============================================================
|
|
462
|
+
# Step 4: mcp.json 注册 (url 模式)
|
|
463
|
+
# ============================================================
|
|
464
|
+
|
|
465
|
+
def lanhu_mcp_url(developer_name='Developer'):
|
|
466
|
+
"""生成蓝湖 MCP 的 url (HTTP Streamable, role/name 作为 query)。"""
|
|
467
|
+
import urllib.parse
|
|
468
|
+
name = urllib.parse.quote(developer_name or 'Developer')
|
|
469
|
+
return f'http://localhost:{LANHU_PORT}{LANHU_MCP_PATH}?role=Developer&name={name}'
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _detect_python_command():
|
|
473
|
+
"""探测本机可用的 python 命令 (跨平台)。win: py -3 > python; mac: python3。
|
|
474
|
+
复用 install_qoderwork 的逻辑, 但本文件独立实现避免循环依赖。"""
|
|
475
|
+
for cand in (('py', ['-3']), ('python3', []), ('python', [])):
|
|
476
|
+
name, extra = cand
|
|
477
|
+
if not shutil.which(name):
|
|
478
|
+
continue
|
|
479
|
+
cmd = [name] + extra + ['--version']
|
|
480
|
+
try:
|
|
481
|
+
r = run(cmd)
|
|
482
|
+
if r.returncode == 0:
|
|
483
|
+
return ' '.join([name] + extra)
|
|
484
|
+
except (OSError, subprocess.SubprocessError):
|
|
485
|
+
continue
|
|
486
|
+
return 'python'
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _lanhu_stdio_entry():
|
|
490
|
+
"""构造 STDIO 模式的 mcp.json lanhu 条目 (launcher 模式, 不写死路径)。
|
|
491
|
+
|
|
492
|
+
用统一启动器 mcp_launcher.py: command=python命令名, args=[launcher, "lanhu"]。
|
|
493
|
+
launcher 运行时自定位仓库根 + 找 venv python + 读角色 cookie。
|
|
494
|
+
这样 mcp.json 不含任何写死的仓库路径/venv路径, 跨机器通用。
|
|
495
|
+
"""
|
|
496
|
+
py_cmd = _detect_python_command()
|
|
497
|
+
# launcher 在 ~/.qoderwork/mcp_launcher.py (install 时拷)
|
|
498
|
+
launcher = str(_HOME / '.qoderwork' / 'mcp_launcher.py')
|
|
499
|
+
return {
|
|
500
|
+
'command': py_cmd,
|
|
501
|
+
'args': [launcher, 'lanhu'],
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _is_stdio_entry(entry):
|
|
506
|
+
"""判断 mcp.json 里 lanhu 条目是不是 launcher STDIO 格式 (args 含 mcp_launcher)。"""
|
|
507
|
+
if not isinstance(entry, dict):
|
|
508
|
+
return False
|
|
509
|
+
args = entry.get('args', [])
|
|
510
|
+
return 'command' in entry and any('mcp_launcher.py' in str(a) for a in args)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def register_lanhu_mcp(developer_name='Developer', dry=False):
|
|
514
|
+
"""在 ~/.qoderwork/mcp.json 注册蓝湖 MCP (STDIO 模式, QoderWork 自动开关)。
|
|
515
|
+
|
|
516
|
+
STDIO 模式: command 指向 wrapper, QoderWork 开自动拉起、关自动杀。
|
|
517
|
+
cookie 由 wrapper 运行时按角色读 .secrets, 不进 mcp.json (角色隔离)。
|
|
518
|
+
|
|
519
|
+
迁移: 旧的 url 格式 (HTTP) → 升级成 STDIO。
|
|
520
|
+
幂等: 已是 STDIO 格式 → 跳过。
|
|
521
|
+
返回: 'ok-existing' / 'added' / 'upgraded' / 'would-add'
|
|
522
|
+
"""
|
|
523
|
+
if not QODERWORK_MCP_FILE.is_file():
|
|
524
|
+
if dry:
|
|
525
|
+
return 'would-add'
|
|
526
|
+
QODERWORK_MCP_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
527
|
+
|
|
528
|
+
existing = {}
|
|
529
|
+
if QODERWORK_MCP_FILE.is_file():
|
|
530
|
+
try:
|
|
531
|
+
existing = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8'))
|
|
532
|
+
if not isinstance(existing, dict):
|
|
533
|
+
existing = {}
|
|
534
|
+
except (json.JSONDecodeError, OSError):
|
|
535
|
+
existing = {}
|
|
536
|
+
|
|
537
|
+
servers = existing.get('mcpServers', {})
|
|
538
|
+
if not isinstance(servers, dict):
|
|
539
|
+
servers = {}
|
|
540
|
+
|
|
541
|
+
cur = servers.get('lanhu')
|
|
542
|
+
stdio_entry = _lanhu_stdio_entry()
|
|
543
|
+
|
|
544
|
+
if cur and _is_stdio_entry(cur):
|
|
545
|
+
# 已是 STDIO, 但 command/args 可能因换机器变了 (venv 路径), 校准
|
|
546
|
+
if cur.get('command') == stdio_entry['command'] and cur.get('args') == stdio_entry['args']:
|
|
547
|
+
return 'ok-existing'
|
|
548
|
+
# 路径漂移, 更新
|
|
549
|
+
if dry:
|
|
550
|
+
return 'would-add'
|
|
551
|
+
servers['lanhu'] = stdio_entry
|
|
552
|
+
existing['mcpServers'] = servers
|
|
553
|
+
QODERWORK_MCP_FILE.write_text(
|
|
554
|
+
json.dumps(existing, indent=2, ensure_ascii=False) + '\n',
|
|
555
|
+
encoding='utf-8')
|
|
556
|
+
return 'upgraded'
|
|
557
|
+
|
|
558
|
+
if dry:
|
|
559
|
+
return 'would-add'
|
|
560
|
+
|
|
561
|
+
# 新增或从 url(HTTP) 升级到 STDIO
|
|
562
|
+
was_http = bool(cur and 'url' in cur)
|
|
563
|
+
servers['lanhu'] = stdio_entry
|
|
564
|
+
existing['mcpServers'] = servers
|
|
565
|
+
QODERWORK_MCP_FILE.write_text(
|
|
566
|
+
json.dumps(existing, indent=2, ensure_ascii=False) + '\n',
|
|
567
|
+
encoding='utf-8')
|
|
568
|
+
return 'upgraded' if was_http else 'added'
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def unregister_lanhu_mcp():
|
|
572
|
+
"""从 mcp.json 删 lanhu 条目 (best-effort)。"""
|
|
573
|
+
if not QODERWORK_MCP_FILE.is_file():
|
|
574
|
+
return False
|
|
575
|
+
try:
|
|
576
|
+
existing = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8'))
|
|
577
|
+
servers = existing.get('mcpServers', {})
|
|
578
|
+
if 'lanhu' in servers:
|
|
579
|
+
del servers['lanhu']
|
|
580
|
+
existing['mcpServers'] = servers
|
|
581
|
+
QODERWORK_MCP_FILE.write_text(
|
|
582
|
+
json.dumps(existing, indent=2, ensure_ascii=False) + '\n',
|
|
583
|
+
encoding='utf-8',
|
|
584
|
+
)
|
|
585
|
+
return True
|
|
586
|
+
except (json.JSONDecodeError, OSError):
|
|
587
|
+
pass
|
|
588
|
+
return False
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# ============================================================
|
|
592
|
+
# 健康探测: 蓝湖 HTTP 服务是否在跑
|
|
593
|
+
# ============================================================
|
|
594
|
+
|
|
595
|
+
def is_lanhu_running():
|
|
596
|
+
"""探测 localhost:8000 是否在监听 (蓝湖服务是否启动)。"""
|
|
597
|
+
try:
|
|
598
|
+
with socket.create_connection(('127.0.0.1', LANHU_PORT), timeout=1):
|
|
599
|
+
return True
|
|
600
|
+
except (OSError, socket.timeout):
|
|
601
|
+
return False
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
# ============================================================
|
|
605
|
+
# 打印启动指引
|
|
606
|
+
# ============================================================
|
|
607
|
+
|
|
608
|
+
def print_start_hint():
|
|
609
|
+
"""STDIO 模式下, 蓝湖由 QoderWork 自动管, 这个 hint 主要提示怎么验证。"""
|
|
610
|
+
print('\n 蓝湖 MCP (STDIO 模式, QoderWork 自动开关):')
|
|
611
|
+
print(' 开 QoderWork 自动起, 关自动停 — 无需手动 start')
|
|
612
|
+
print(' 验证: 在 QoderWork 里 /wl-design 预览 <蓝湖链接>, 能调通即正常')
|
|
613
|
+
print(' 换角色: 改 .developer + 配新角色 cookie → 重启 QoderWork')
|
|
614
|
+
print(' 调试/多客户端共享 (强制 HTTP): python .qoder/scripts/setup_lanhu.py start --http')
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
# ============================================================
|
|
618
|
+
# 启动蓝湖服务端 (读当前用户 cookie, 以环境变量注入)
|
|
619
|
+
# ============================================================
|
|
620
|
+
|
|
621
|
+
def start_lanhu_server(force_http=False):
|
|
622
|
+
"""启动蓝湖 MCP 服务。
|
|
623
|
+
|
|
624
|
+
默认 (STDIO 模式): 不需要手动 start, QoderWork 自动管。本函数只提示。
|
|
625
|
+
force_http=True: 走旧 HTTP 模式 (前台常驻, 用于调试或多客户端共享)。
|
|
626
|
+
"""
|
|
627
|
+
if not force_http:
|
|
628
|
+
print('=' * 56)
|
|
629
|
+
print(' 蓝湖 MCP 服务启动')
|
|
630
|
+
print('=' * 56)
|
|
631
|
+
# 检测当前 mcp.json 是不是 STDIO
|
|
632
|
+
entry = None
|
|
633
|
+
if QODERWORK_MCP_FILE.is_file():
|
|
634
|
+
try:
|
|
635
|
+
entry = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8')).get('mcpServers', {}).get('lanhu')
|
|
636
|
+
except Exception:
|
|
637
|
+
pass
|
|
638
|
+
if _is_stdio_entry(entry) if entry else False:
|
|
639
|
+
ok('已是 STDIO 模式 — QoderWork 自动开关, 无需手动 start')
|
|
640
|
+
print(' 开 QoderWork 自动拉起蓝湖 (wrapper 读当前角色 cookie), 关自动停')
|
|
641
|
+
print(' 换角色: 改 .developer + 配新角色 cookie → 重启 QoderWork')
|
|
642
|
+
print('')
|
|
643
|
+
print(' 要强制用 HTTP 模式 (调试/多客户端共享):')
|
|
644
|
+
print(f' python .qoder/scripts/setup_lanhu.py start --http')
|
|
645
|
+
return 0
|
|
646
|
+
# 非 STDIO (旧 HTTP 或没注册), 提示升级
|
|
647
|
+
warn('当前不是 STDIO 模式, 手动 start 只支持 HTTP')
|
|
648
|
+
print(' 建议升级 STDIO (QoderWork 自动开关): python .qoder/scripts/setup_lanhu.py')
|
|
649
|
+
print(' 或强制 HTTP: python .qoder/scripts/setup_lanhu.py start --http')
|
|
650
|
+
return 1
|
|
651
|
+
|
|
652
|
+
# === HTTP 模式 (force_http=True, 降级/调试用) ===
|
|
653
|
+
print('=' * 56)
|
|
654
|
+
print(' 启动蓝湖 MCP 服务端 (HTTP 模式, 降级/调试)')
|
|
655
|
+
print('=' * 56)
|
|
656
|
+
|
|
657
|
+
# 1. 源码 + venv 是否就绪
|
|
658
|
+
if not LANHU_SERVER_SCRIPT.is_file():
|
|
659
|
+
fail(f'蓝湖服务脚本不存在: {LANHU_SERVER_SCRIPT}')
|
|
660
|
+
print(' 先跑: python .qoder/scripts/setup_lanhu.py')
|
|
661
|
+
return 1
|
|
662
|
+
py = _venv_python(LANHU_VENV)
|
|
663
|
+
if not py.is_file():
|
|
664
|
+
fail(f'venv python 不存在: {py}')
|
|
665
|
+
print(' 先跑: python .qoder/scripts/setup_lanhu.py')
|
|
666
|
+
return 1
|
|
667
|
+
|
|
668
|
+
# 2. 当前用户 + cookie
|
|
669
|
+
dev = _current_developer()
|
|
670
|
+
env_file = lanhu_env_file(dev)
|
|
671
|
+
env_cookie = os.environ.get('LANHU_COOKIE', '').strip()
|
|
672
|
+
cookie_source = 'env' if env_cookie else 'file'
|
|
673
|
+
cookie = env_cookie or _read_env_cookie(dev)
|
|
674
|
+
|
|
675
|
+
print(f' 当前开发者: {dev or "(未初始化, 用 default)"}')
|
|
676
|
+
if cookie_source == 'env':
|
|
677
|
+
print(f' cookie 来源: 环境变量 LANHU_COOKIE (临时, 不落盘)')
|
|
678
|
+
else:
|
|
679
|
+
print(f' cookie 来源: {env_file} (主线, 落盘)')
|
|
680
|
+
|
|
681
|
+
if not cookie or cookie == COOKIE_PLACEHOLDER:
|
|
682
|
+
fail('cookie 未配置')
|
|
683
|
+
if cookie_source == 'env':
|
|
684
|
+
print(' 环境变量 LANHU_COOKIE 是空的或占位')
|
|
685
|
+
else:
|
|
686
|
+
print(f' 编辑 {env_file}, 把 {COOKIE_PLACEHOLDER} 换成真 cookie')
|
|
687
|
+
print(' 获取: lanhuapp.com 登录 → F12 → Network → Cookie 头复制')
|
|
688
|
+
return 1
|
|
689
|
+
print(f' cookie: 已配置 (长度 {len(cookie)})')
|
|
690
|
+
|
|
691
|
+
# 3. 已在跑就别重复启
|
|
692
|
+
if is_lanhu_running():
|
|
693
|
+
warn(f'蓝湖服务已在跑 (localhost:{LANHU_PORT}), 不重复启动')
|
|
694
|
+
return 0
|
|
695
|
+
|
|
696
|
+
# 4. 以环境变量注入 cookie 启动 (前台常驻, Ctrl+C 退出)
|
|
697
|
+
print(f'\n 启动: {py} {LANHU_SERVER_SCRIPT}')
|
|
698
|
+
print(f' HTTP: http://localhost:{LANHU_PORT}{LANHU_MCP_PATH}')
|
|
699
|
+
print(' (常驻前台, 用完 Ctrl+C 关闭。别关这个终端。)\n')
|
|
700
|
+
env = os.environ.copy()
|
|
701
|
+
env['LANHU_COOKIE'] = cookie
|
|
702
|
+
# 不捕获输出 —— 服务端日志直接打到当前终端, 用户能看到请求/报错
|
|
703
|
+
try:
|
|
704
|
+
return subprocess.call([str(py), str(LANHU_SERVER_SCRIPT)], env=env)
|
|
705
|
+
except OSError as e:
|
|
706
|
+
fail(f'启动失败: {e}')
|
|
707
|
+
return 1
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
# ============================================================
|
|
711
|
+
# 主流程
|
|
712
|
+
# ============================================================
|
|
713
|
+
|
|
714
|
+
def configure_lanhu(developer_name='Developer', dry=False, skip_install=False):
|
|
715
|
+
"""完整引导: clone → 依赖 → cookie → mcp.json 注册。
|
|
716
|
+
|
|
717
|
+
返回 dict 报告各步状态。
|
|
718
|
+
"""
|
|
719
|
+
report = {'clone': 'skipped', 'deps': 'skipped',
|
|
720
|
+
'cookie': 'skipped', 'mcp': 'skipped'}
|
|
721
|
+
|
|
722
|
+
if skip_install:
|
|
723
|
+
print('\n--- 蓝湖 MCP (--skip-install, 只注册 mcp.json) ---')
|
|
724
|
+
else:
|
|
725
|
+
print('\n' + '=' * 56)
|
|
726
|
+
print(' 蓝湖 MCP 引导')
|
|
727
|
+
print('=' * 56)
|
|
728
|
+
|
|
729
|
+
# 1. clone
|
|
730
|
+
status, _ = ensure_clone(dry=dry)
|
|
731
|
+
report['clone'] = status
|
|
732
|
+
if status == 'failed':
|
|
733
|
+
print('\n蓝湖源码获取失败, 后续步骤跳过。')
|
|
734
|
+
return report
|
|
735
|
+
|
|
736
|
+
# 2. 依赖
|
|
737
|
+
report['deps'] = ensure_deps(dry=dry)
|
|
738
|
+
|
|
739
|
+
# 3. cookie (即使 skip_install 也引导, 因为 mcp.json 注册后需要 cookie 才能用)
|
|
740
|
+
if not skip_install:
|
|
741
|
+
report['cookie'] = ensure_cookie(dry=dry)
|
|
742
|
+
|
|
743
|
+
# 4. mcp.json 注册 (STDIO 模式, QoderWork 自动开关)
|
|
744
|
+
print('\n--- 4. 注册到 QoderWork mcp.json (STDIO 自动开关) ---')
|
|
745
|
+
status = register_lanhu_mcp(developer_name, dry=dry)
|
|
746
|
+
report['mcp'] = status
|
|
747
|
+
if status == 'ok-existing':
|
|
748
|
+
ok(f'蓝湖 MCP 已注册 (STDIO): {QODERWORK_MCP_FILE}')
|
|
749
|
+
print(' 开 QoderWork 自动起, 关自动停, 无需手动 start')
|
|
750
|
+
elif status in ('added', 'upgraded'):
|
|
751
|
+
tag = '已写入' if status == 'added' else '已升级(HTTP→STDIO)'
|
|
752
|
+
ok(f'{tag} 蓝湖 MCP: {QODERWORK_MCP_FILE}')
|
|
753
|
+
print(' STDIO 模式: 开 QoderWork 自动起, 关自动停')
|
|
754
|
+
print(f' command → wrapper → 读 {developer_name} 的 cookie 拉起 lanhu')
|
|
755
|
+
elif status == 'would-add':
|
|
756
|
+
print(f' [MISS] 未注册, 安装时会写入 {QODERWORK_MCP_FILE}')
|
|
757
|
+
|
|
758
|
+
return report
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def print_check_report():
|
|
762
|
+
"""--check 模式: 体检报告 (不改动)。"""
|
|
763
|
+
print('=' * 56)
|
|
764
|
+
print(' 蓝湖 MCP 体检')
|
|
765
|
+
print('=' * 56)
|
|
766
|
+
|
|
767
|
+
# clone
|
|
768
|
+
print('\n--- 1. 源码 ---')
|
|
769
|
+
if LANHU_DIR.is_dir() and (LANHU_DIR / '.git').is_dir():
|
|
770
|
+
ok(f'已 clone: {LANHU_DIR}')
|
|
771
|
+
# 检查关键文件
|
|
772
|
+
server_file = LANHU_DIR / 'lanhu_mcp_server.py'
|
|
773
|
+
if server_file.is_file():
|
|
774
|
+
ok(f'入口文件在: {server_file}')
|
|
775
|
+
else:
|
|
776
|
+
warn(f'入口文件缺失: {server_file} (lanhu-mcp 版本结构可能变了)')
|
|
777
|
+
else:
|
|
778
|
+
print(f' [MISS] 未 clone (跑 setup.py 或本脚本安装)')
|
|
779
|
+
|
|
780
|
+
# 依赖
|
|
781
|
+
print('\n--- 2. 依赖 ---')
|
|
782
|
+
if LANHU_VENV.is_dir() and _venv_python(LANHU_VENV).is_file():
|
|
783
|
+
marker = LANHU_DIR / '.deps-installed'
|
|
784
|
+
# 实测验证: 尝试导入 fastmcp (lanhu-mcp 的核心依赖, 缺它一定跑不起来)
|
|
785
|
+
# marker 只是快速跳过标记, 真相以能否导入为准
|
|
786
|
+
py = _venv_python(LANHU_VENV)
|
|
787
|
+
r = run([str(py), '-c', 'import fastmcp; print(fastmcp.__version__)'])
|
|
788
|
+
deps_importable = r.returncode == 0 and r.stdout.strip()
|
|
789
|
+
if deps_importable:
|
|
790
|
+
extra = f' (fastmcp {r.stdout.strip()})'
|
|
791
|
+
if marker.is_file():
|
|
792
|
+
ok(f'venv + 依赖已装: {LANHU_VENV}{extra}')
|
|
793
|
+
else:
|
|
794
|
+
ok(f'venv + 依赖已装 (marker 缺失但实测可导入): {LANHU_VENV}{extra}')
|
|
795
|
+
print(f' (补写 marker 以便后续快速跳过)')
|
|
796
|
+
try:
|
|
797
|
+
marker.write_text('deps verified by import\n', encoding='utf-8')
|
|
798
|
+
except OSError:
|
|
799
|
+
pass
|
|
800
|
+
elif marker.is_file():
|
|
801
|
+
warn(f'marker 在但 fastmcp 导入失败, 依赖可能损坏: {LANHU_VENV}')
|
|
802
|
+
print(f' 重装: python .qoder/scripts/setup_lanhu.py')
|
|
803
|
+
else:
|
|
804
|
+
warn(f'venv 存在但依赖未装完 (fastmcp 不可导入): {LANHU_VENV}')
|
|
805
|
+
print(f' 重装: python .qoder/scripts/setup_lanhu.py')
|
|
806
|
+
else:
|
|
807
|
+
print(f' [MISS] venv 未建 (跑 setup.py 或本脚本安装)')
|
|
808
|
+
|
|
809
|
+
# cookie (按开发者隔离)
|
|
810
|
+
print('\n--- 3. Cookie ---')
|
|
811
|
+
dev = _current_developer()
|
|
812
|
+
env_file = lanhu_env_file(dev)
|
|
813
|
+
if dev:
|
|
814
|
+
print(f' 当前开发者: {dev}')
|
|
815
|
+
cookie = _read_env_cookie(dev)
|
|
816
|
+
if cookie and cookie != COOKIE_PLACEHOLDER:
|
|
817
|
+
ok(f'蓝湖 cookie 已配置 ({env_file})')
|
|
818
|
+
elif cookie == COOKIE_PLACEHOLDER:
|
|
819
|
+
warn(f'占位 cookie ({COOKIE_PLACEHOLDER}), 需要换成真的')
|
|
820
|
+
print(f' 编辑: {env_file}')
|
|
821
|
+
else:
|
|
822
|
+
print(f' [MISS] 没有 LANHU_COOKIE ({env_file})')
|
|
823
|
+
|
|
824
|
+
# mcp.json (STDIO 模式检测)
|
|
825
|
+
print('\n--- 4. QoderWork 注册 ---')
|
|
826
|
+
if QODERWORK_MCP_FILE.is_file():
|
|
827
|
+
try:
|
|
828
|
+
cfg = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8'))
|
|
829
|
+
servers = cfg.get('mcpServers', {})
|
|
830
|
+
entry = servers.get('lanhu')
|
|
831
|
+
if not entry:
|
|
832
|
+
print(f' [MISS] mcp.json 没有 lanhu 条目')
|
|
833
|
+
elif _is_stdio_entry(entry):
|
|
834
|
+
launcher_ok = (_HOME / '.qoderwork' / 'mcp_launcher.py').is_file()
|
|
835
|
+
ok(f'已注册 (STDIO launcher, 自动开关): command={entry.get("command","?")}')
|
|
836
|
+
ok(f' launcher 存在: {"✓" if launcher_ok else "✗ 缺 ~/.qoderwork/mcp_launcher.py (重跑 install_qoderwork)"}')
|
|
837
|
+
print(' 开 QoderWork 自动起, 关自动停 (无需手动 start)')
|
|
838
|
+
elif 'command' in entry:
|
|
839
|
+
# 旧 STDIO (写死 venv 路径) 或其他 command 格式
|
|
840
|
+
warn(f'旧 STDIO 格式 (command 写死路径), 建议升级 launcher 模式')
|
|
841
|
+
print(' 升级: python .qoder/scripts/install_qoderwork.py')
|
|
842
|
+
else:
|
|
843
|
+
warn(f'旧 HTTP 模式 (url={entry.get("url","?")[:50]}...), 需手动 start')
|
|
844
|
+
print(' 升级 STDIO: python .qoder/scripts/setup_lanhu.py')
|
|
845
|
+
except (json.JSONDecodeError, OSError):
|
|
846
|
+
warn(f'mcp.json 解析失败: {QODERWORK_MCP_FILE}')
|
|
847
|
+
else:
|
|
848
|
+
print(f' [MISS] mcp.json 不存在: {QODERWORK_MCP_FILE}')
|
|
849
|
+
|
|
850
|
+
# 服务状态 (STDIO 模式由 QoderWork 管, 不常驻端口; 这里只提示)
|
|
851
|
+
print('\n--- 5. 服务状态 ---')
|
|
852
|
+
entry = None
|
|
853
|
+
if QODERWORK_MCP_FILE.is_file():
|
|
854
|
+
try:
|
|
855
|
+
entry = json.loads(QODERWORK_MCP_FILE.read_text(encoding='utf-8')).get('mcpServers', {}).get('lanhu')
|
|
856
|
+
except Exception:
|
|
857
|
+
pass
|
|
858
|
+
if _is_stdio_entry(entry) if entry else False:
|
|
859
|
+
ok('STDIO 模式: QoderWork 启动时自动拉起 (不占常驻端口)')
|
|
860
|
+
print(' (要验证: 开 QoderWork 后 /wl-design 预览 <蓝湖链接> 能调通即正常)')
|
|
861
|
+
elif is_lanhu_running():
|
|
862
|
+
ok(f'蓝湖服务在跑 (HTTP, localhost:{LANHU_PORT}) — 旧模式, 建议升级 STDIO')
|
|
863
|
+
else:
|
|
864
|
+
print(' STDIO 模式下服务由 QoderWork 按需拉起, 这里不显示常驻状态')
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def do_uninstall():
|
|
868
|
+
"""删 external/lanhu-mcp/ + mcp.json lanhu 条目。"""
|
|
869
|
+
print('=' * 56)
|
|
870
|
+
print(' 蓝湖 MCP 卸载')
|
|
871
|
+
print('=' * 56)
|
|
872
|
+
|
|
873
|
+
# 1. mcp.json 条目
|
|
874
|
+
print('\n--- 1. 移除 mcp.json 条目 ---')
|
|
875
|
+
if unregister_lanhu_mcp():
|
|
876
|
+
ok(f'已从 mcp.json 删除 lanhu 条目')
|
|
877
|
+
else:
|
|
878
|
+
print(' mcp.json 无 lanhu 条目 (无需删除)')
|
|
879
|
+
|
|
880
|
+
# 2. external/lanhu-mcp/
|
|
881
|
+
print('\n--- 2. 删除源码 ---')
|
|
882
|
+
if LANHU_DIR.is_dir():
|
|
883
|
+
if ask(f'删除 {LANHU_DIR}? (含 venv + chromium 缓存, 不可恢复)', 'n'):
|
|
884
|
+
try:
|
|
885
|
+
shutil.rmtree(LANHU_DIR)
|
|
886
|
+
ok(f'已删除: {LANHU_DIR}')
|
|
887
|
+
except OSError as e:
|
|
888
|
+
fail(f'删除失败: {e}')
|
|
889
|
+
else:
|
|
890
|
+
print(' 跳过 (保留源码)')
|
|
891
|
+
else:
|
|
892
|
+
print(f' 源码目录不存在: {LANHU_DIR}')
|
|
893
|
+
|
|
894
|
+
# 3. 清理空的 external/
|
|
895
|
+
if EXTERNAL_DIR.is_dir():
|
|
896
|
+
try:
|
|
897
|
+
remaining = list(EXTERNAL_DIR.iterdir())
|
|
898
|
+
if not remaining:
|
|
899
|
+
EXTERNAL_DIR.rmdir()
|
|
900
|
+
ok(f'已删除空目录: {EXTERNAL_DIR}')
|
|
901
|
+
except OSError:
|
|
902
|
+
pass
|
|
903
|
+
|
|
904
|
+
print('\n✓ 蓝湖 MCP 卸载完成。')
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def main():
|
|
908
|
+
parser = argparse.ArgumentParser(
|
|
909
|
+
description='蓝湖 MCP 引导器: clone + 依赖 + cookie + mcp.json 注册',
|
|
910
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
911
|
+
epilog='''示例:
|
|
912
|
+
python .qoder/scripts/setup_lanhu.py # 引导 + 注册 STDIO (幂等, 自动开关)
|
|
913
|
+
python .qoder/scripts/setup_lanhu.py --check # 只体检
|
|
914
|
+
python .qoder/scripts/setup_lanhu.py start # 提示 STDIO 状态 (默认无需手动)
|
|
915
|
+
python .qoder/scripts/setup_lanhu.py start --http # 强制 HTTP 模式 (调试/多客户端)
|
|
916
|
+
python .qoder/scripts/setup_lanhu.py --skip-install # 只注册 mcp.json
|
|
917
|
+
python .qoder/scripts/setup_lanhu.py --uninstall # 卸载
|
|
918
|
+
''')
|
|
919
|
+
parser.add_argument('command', nargs='?', default=None,
|
|
920
|
+
help='子命令: start = 提示服务状态 (STDIO 自动, 加 --http 强制 HTTP)')
|
|
921
|
+
parser.add_argument('--check', action='store_true', help='只体检不改动')
|
|
922
|
+
parser.add_argument('--uninstall', action='store_true', help='卸载 (删源码 + mcp.json 条目)')
|
|
923
|
+
parser.add_argument('--skip-install', action='store_true',
|
|
924
|
+
help='只注册 mcp.json, 不 clone/装依赖')
|
|
925
|
+
parser.add_argument('--http', action='store_true',
|
|
926
|
+
help='start 时强制走 HTTP 模式 (调试/多客户端共享; 默认 STDIO 自动开关)')
|
|
927
|
+
parser.add_argument('--name', default='Developer', help='开发者名 (用于蓝湖 url 的 name 参数)')
|
|
928
|
+
args = parser.parse_args()
|
|
929
|
+
|
|
930
|
+
# 子命令: start (提示服务状态 / 强制 HTTP)
|
|
931
|
+
if args.command == 'start':
|
|
932
|
+
return start_lanhu_server(force_http=args.http)
|
|
933
|
+
if args.command and args.command not in (None,):
|
|
934
|
+
fail(f'未知子命令: {args.command}')
|
|
935
|
+
print(' 可用: start [--http]')
|
|
936
|
+
return 1
|
|
937
|
+
|
|
938
|
+
if args.check:
|
|
939
|
+
print_check_report()
|
|
940
|
+
return 0
|
|
941
|
+
|
|
942
|
+
if args.uninstall:
|
|
943
|
+
do_uninstall()
|
|
944
|
+
return 0
|
|
945
|
+
|
|
946
|
+
report = configure_lanhu(
|
|
947
|
+
developer_name=args.name,
|
|
948
|
+
dry=False,
|
|
949
|
+
skip_install=args.skip_install,
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
# 汇总
|
|
953
|
+
print('\n' + '=' * 56)
|
|
954
|
+
print(' 蓝湖 MCP 引导完成')
|
|
955
|
+
print('=' * 56)
|
|
956
|
+
print(f' 源码: {report["clone"]}')
|
|
957
|
+
print(f' 依赖: {report["deps"]}')
|
|
958
|
+
print(f' Cookie: {report["cookie"]}')
|
|
959
|
+
print(f' 注册: {report["mcp"]}')
|
|
960
|
+
|
|
961
|
+
if report['cookie'] == 'placeholder':
|
|
962
|
+
print(f'\n ⚠ 下一步: 编辑 {lanhu_env_file()}, 把 {COOKIE_PLACEHOLDER} 换成真 cookie')
|
|
963
|
+
|
|
964
|
+
if report['mcp'] in ('added', 'ok-existing') and not is_lanhu_running():
|
|
965
|
+
print_start_hint()
|
|
966
|
+
|
|
967
|
+
print('\n QoderWork 里用命令触发蓝湖 (非侵入式, 必须走命令):')
|
|
968
|
+
print(' /wl-design 预览 <蓝湖链接> → design-import 调 mcp__lanhu__* (命令内授权)')
|
|
969
|
+
return 0
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
if __name__ == '__main__':
|
|
973
|
+
sys.exit(main())
|