hmdev-cli 1.0.3 → 1.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmdev-cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "HarmonyOS 开发 CLI 工具 — 文档查询、项目构建、设备部署",
5
5
  "keywords": [
6
6
  "harmonyos",
package/python/builder.py CHANGED
@@ -193,6 +193,10 @@ class HDCTool:
193
193
  )
194
194
  return subprocess.run([self._hdc_path] + args, capture_output=True, text=True, timeout=timeout)
195
195
 
196
+ @staticmethod
197
+ def succeeded(result: subprocess.CompletedProcess) -> bool:
198
+ return result.returncode == 0 and "[Fail]" not in result.stdout and "[Fail]" not in result.stderr
199
+
196
200
  def list_devices(self) -> list[dict]:
197
201
  result = self._run(["list", "targets"])
198
202
  devices = []
@@ -221,5 +225,5 @@ class HDCTool:
221
225
  args.extend(["shell", "aa", "start", "-a", ability, "-b", bundle])
222
226
  return self._run(args, timeout=30)
223
227
 
224
- def connect_wireless(self, ip_port: str, timeout: int = 10) -> subprocess.CompletedProcess:
228
+ def connect_wireless(self, ip_port: str, timeout: int = 30) -> subprocess.CompletedProcess:
225
229
  return self._run(["tconn", ip_port], timeout=timeout)
package/python/cli.py CHANGED
@@ -16,6 +16,8 @@ Usage:
16
16
  import asyncio
17
17
  import json
18
18
  import re
19
+ import shutil
20
+ import subprocess
19
21
  import subprocess
20
22
  import time
21
23
  from argparse import ArgumentParser, RawDescriptionHelpFormatter, SUPPRESS
@@ -23,6 +25,8 @@ from html.parser import HTMLParser
23
25
  from typing import Any
24
26
 
25
27
  import httpx
28
+ from rapidfuzz import fuzz
29
+ import jieba
26
30
 
27
31
  from builder import HvigorTool, HDCTool
28
32
  from config import Config
@@ -293,6 +297,67 @@ async def build_index() -> dict[str, Any]:
293
297
  return result
294
298
 
295
299
 
300
+ # ── Search Ranking ─────────────────────────────────────────────────────────────
301
+
302
+ def compute_relevance_score(query: str, title: str, object_id: str, catalog_name: str) -> float:
303
+ """
304
+ Compute a relevance score for a document against the query.
305
+
306
+ Combines:
307
+ - Exact substring/title/ID/category match (highest weight)
308
+ - Fuzzy string matching via rapidfuzz (partial_ratio, token_sort, token_set)
309
+ - Chinese word segmentation overlap via jieba (semantic-like matching)
310
+ - Word prefix bonus
311
+
312
+ Higher score = more relevant.
313
+ """
314
+ q = query.lower().strip()
315
+ t = title.lower()
316
+ o = object_id.lower()
317
+ c = catalog_name.lower()
318
+
319
+ score = 0.0
320
+
321
+ # ── 1. Exact match (strongest signal) ──
322
+ if t == q:
323
+ score += 5.0
324
+ elif t.startswith(q):
325
+ score += 4.0
326
+ elif q in t:
327
+ score += 3.0
328
+
329
+ if q in o:
330
+ score += 1.5
331
+ if q in c:
332
+ score += 0.5
333
+
334
+ # ── 2. Fuzzy match on title ──
335
+ score += (fuzz.partial_ratio(q, t) / 100.0) * 2.0
336
+ score += (fuzz.token_sort_ratio(q, t) / 100.0) * 1.5
337
+ score += (fuzz.token_set_ratio(q, t) / 100.0) * 1.0
338
+
339
+ # ── 3. Fuzzy match on object_id ──
340
+ score += (fuzz.partial_ratio(q, o) / 100.0) * 0.8
341
+
342
+ # ── 4. Word overlap via jieba (handles Chinese segmentation) ──
343
+ q_words = set(w for w in jieba.lcut(q) if w.strip())
344
+ t_words = set(w for w in jieba.lcut(t) if w.strip())
345
+ if q_words and t_words:
346
+ common = q_words & t_words
347
+ score += (len(common) / len(q_words)) * 2.0
348
+
349
+ # ── 5. Prefix match: query word is prefix of title word ──
350
+ for qw in q_words:
351
+ if len(qw) < 2:
352
+ continue
353
+ for tw in t_words:
354
+ if tw != qw and tw.startswith(qw):
355
+ score += 0.5
356
+ break
357
+
358
+ return score
359
+
360
+
296
361
  # ── CLI Helpers ────────────────────────────────────────────────────────────────
297
362
 
298
363
  def parse_doc_url(url: str) -> tuple[str, str]:
@@ -343,34 +408,58 @@ async def cmd_index(args):
343
408
 
344
409
  async def cmd_search(args):
345
410
  index = await build_index()
346
- query_lower = args.query.lower()
347
- results = []
411
+ query = args.query.strip()
412
+ if not query:
413
+ print("请提供搜索关键词。")
414
+ return
415
+
416
+ query_lower = query.lower()
348
417
  seen = set()
418
+ scored = []
349
419
 
350
420
  for page in index.get("all_pages", []):
351
- title = page.get("title", "").lower()
352
- obj_id = page.get("object_id", "").lower()
353
- if query_lower in title or query_lower in obj_id or query_lower in page.get("catalog_name", ""):
421
+ title = page.get("title", "")
422
+ obj_id = page.get("object_id", "")
423
+ catalog = page.get("catalog_name", "")
424
+
425
+ score = compute_relevance_score(query, title, obj_id, catalog)
426
+
427
+ # Include if exact match exists or fuzzy score is significant
428
+ if (query_lower in title.lower()
429
+ or query_lower in obj_id.lower()
430
+ or query_lower in catalog.lower()
431
+ or score >= 1.5):
354
432
  if page.get("url") not in seen:
355
433
  seen.add(page["url"])
356
- results.append(page)
434
+ page = dict(page)
435
+ page["_score"] = round(score, 2)
436
+ scored.append(page)
437
+
438
+ # Sort: higher score first, shorter title as tiebreaker
439
+ scored.sort(key=lambda p: (-p["_score"], len(p.get("title", ""))))
357
440
 
358
441
  if args.json:
359
- print_json({"query": args.query, "total": len(results), "results": results[:50]})
442
+ out = {
443
+ "query": args.query,
444
+ "total": len(scored),
445
+ "results": scored[:50],
446
+ }
447
+ print_json(out)
360
448
  return
361
449
 
362
- if not results:
450
+ if not scored:
363
451
  print(f"未找到与 '{args.query}' 相关的文档。")
364
452
  print(f"可用分类: {', '.join(f'{v}({k})' for k, v in CATALOGS.items())}")
365
453
  return
366
454
 
367
- print(f"搜索结果: '{args.query}' (共 {len(results)} 篇)\n")
368
- for page in results[:30]:
455
+ print(f"搜索结果: '{args.query}' (共 {len(scored)} 篇)\n")
456
+ for page in scored[:30]:
369
457
  cat = CATALOGS.get(page.get("catalog_name", ""), page.get("catalog_name", ""))
370
- print(f" [{cat}] {page['title']}")
371
- print(f" {page['url']}")
372
- if len(results) > 30:
373
- print(f"\n...及另外 {len(results) - 30} 篇")
458
+ bar = "█" * min(int(page["_score"]), 10) + "░" * (10 - min(int(page["_score"]), 10))
459
+ print(f" {bar} [{cat}] {page['title']}")
460
+ print(f" {page['url']}")
461
+ if len(scored) > 30:
462
+ print(f"\n...及另外 {len(scored) - 30} 篇")
374
463
 
375
464
 
376
465
  async def cmd_get(args):
@@ -477,7 +566,7 @@ async def cmd_deploy(args):
477
566
  if args.tconn:
478
567
  print(f"[hmdev] 无线连接: {args.tconn}")
479
568
  tr = hdc.connect_wireless(args.tconn)
480
- if tr.returncode != 0:
569
+ if not HDCTool.succeeded(tr):
481
570
  print(f"[hmdev] ❌ 无线连接失败: {tr.stderr.strip()}")
482
571
  return
483
572
  print(f"[hmdev] ✅ 无线连接成功")
@@ -493,7 +582,7 @@ async def cmd_deploy(args):
493
582
 
494
583
  print(f"[hmdev] 正在安装: {hap_path}")
495
584
  ir = hdc.install_hap(hap_path, device_id)
496
- if ir.returncode != 0:
585
+ if not HDCTool.succeeded(ir):
497
586
  err = ir.stderr.strip() or ir.stdout.strip()
498
587
  print(f"[hmdev] ❌ 安装失败: {err}")
499
588
  return
@@ -507,7 +596,7 @@ async def cmd_deploy(args):
507
596
  else:
508
597
  print(f"[hmdev] 正在启动: {bundle}")
509
598
  sr = hdc.start_app(bundle, args.ability, device_id)
510
- if sr.returncode != 0:
599
+ if not HDCTool.succeeded(sr):
511
600
  print(f"[hmdev] ❌ 启动失败: {sr.stderr.strip()}")
512
601
  else:
513
602
  print(f"[hmdev] ✅ 应用已启动")
@@ -668,6 +757,20 @@ async def cmd_config(args):
668
757
  print()
669
758
 
670
759
 
760
+ async def cmd_update(args):
761
+ npm = shutil.which("npm") or shutil.which("npm.cmd")
762
+ if not npm:
763
+ print("[hmdev] ❌ 未找到 npm。请确保 Node.js 已安装。")
764
+ return
765
+ print("[hmdev] 正在更新 hmdev-cli ...")
766
+ result = subprocess.run([npm, "update", "-g", "hmdev-cli"], capture_output=True, text=True)
767
+ if result.returncode == 0:
768
+ print("[hmdev] ✅ 更新成功")
769
+ else:
770
+ err = result.stderr.strip() or result.stdout.strip()
771
+ print(f"[hmdev] ❌ 更新失败: {err}")
772
+
773
+
671
774
  # ── CLI Registration ──────────────────────────────────────────────────────────
672
775
 
673
776
  CLI_COMMANDS = {
@@ -682,6 +785,7 @@ CLI_COMMANDS = {
682
785
  "run": cmd_run,
683
786
  "connect": cmd_connect,
684
787
  "config": cmd_config,
788
+ "update": cmd_update,
685
789
  }
686
790
 
687
791
 
@@ -765,6 +869,10 @@ def build_cli_parser():
765
869
  p.add_argument("--reset", metavar="KEY", help="重置配置项")
766
870
  p.add_argument("--json", action="store_true", dest="json", help=SUPPRESS)
767
871
 
872
+ # update
873
+ p = sub.add_parser("update", help="更新 hmdev-cli 到最新版本 (npm update -g)")
874
+ p.add_argument("--json", action="store_true", dest="json", help=SUPPRESS)
875
+
768
876
  return parser
769
877
 
770
878
 
package/scripts/runner.js CHANGED
@@ -102,8 +102,8 @@ async function ensurePython() {
102
102
  });
103
103
  } catch { /* non-fatal */ }
104
104
 
105
- console.error(`${TAG} 正在安装 Python 依赖 (httpx)...`);
106
- execSync(`"${getVenvPip()}" install httpx`, {
105
+ console.error(`${TAG} 正在安装 Python 依赖 (httpx, rapidfuzz, jieba)...`);
106
+ execSync(`"${getVenvPip()}" install httpx rapidfuzz jieba`, {
107
107
  stdio: 'pipe',
108
108
  timeout: 120000,
109
109
  env: cleanEnv,