@harmonyos-arkts/opencode-plugin 0.0.10 → 0.0.12

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +449 -136
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  - **HarmonyOS设计专家**:为HarmonyOS应用和原子化服务创建详细的PRD设计文档
9
9
  - **HarmonyOS开发专家**:根据设计文档遵循HarmonyOS规范实现功能
10
10
  - **HarmonyOS构建**:使用harmonyos-hvigor技能构建项目并确保编译成功
11
- - **HarmonyOS文档查询**:通过 `harmony-doc-search` 搜索华为开发者文档(含官方文档与社区内容),通过 `harmony-doc-view` 分页查看文档全文
11
+ - **HarmonyOS文档查询**:通过 `harmony-doc` 工具搜索和查看华为开发者文档(含官方文档与社区内容),支持关键词搜索和分页查看全文
12
12
 
13
13
  ## 快速开始
14
14
 
package/dist/index.js CHANGED
@@ -45333,58 +45333,49 @@ function convert_search_result(searchResults, prefix) {
45333
45333
  });
45334
45334
  }
45335
45335
 
45336
- // src/tools/skill-search/skill-search-tool.ts
45337
- function skillSearchTool(managers) {
45338
- return tool({
45339
- description: "Search for relevant documents within the harmonyos-atomic-dev skill directory by keywords. Returns the top K most relevant document snippets from the skill directory, ranked by keyword match frequency. Use this tool instead of Glob/Grep when you need to find specific knowledge or documentation within harmonyos-atomic-dev skill. The results include experience_file_path (path to the matching experience document best practices) and ets_file_path (path to the ETS code examples and SDK API file). IMPORTANT: Each scenario only supports ONE tool call. Do NOT call this tool multiple times.",
45340
- args: {
45341
- skill_path: tool.schema.string("Absolute path to the harmonyos-atomic-dev skill directory. IMPORTANT: this path should not contain any prefix or suffix like 'file://' or 'SKILL.md'."),
45342
- query: tool.schema.string("A decomposed requirement or intent describing what you want to find, broken down into searchable keywords separated by spaces. QUERY TIPS: For best results, decompose your intent into short space-separated keywords instead of long sentences. Include: 1) Kit or component name (e.g. ScanKit, AdsKit, ShareKit), 2) specific API or method names (e.g. scanBarcode, loadAd, ShareController), 3) feature keywords (e.g. \u626B\u7801, \u5E7F\u544A\u52A0\u8F7D, \u5206\u4EAB). Example: 'ScanKit \u626B\u7801 scanBarcode startScanForResult' instead of '\u5E2E\u6211\u5B9E\u73B0\u4E00\u4E2A\u626B\u7801\u529F\u80FD'. "),
45343
- topK: tool.schema.number("Maximum number of top-ranked documents to return. Actual results may be fewer depending on query relevance.").min(1).max(3).default(3)
45344
- },
45345
- execute: async (args, context) => {
45346
- try {
45347
- const results = await searchSkill(args.skill_path, args.query, args.topK);
45348
- if (results.length === 0) {
45349
- return "No relative items found in the skill directory.";
45350
- }
45351
- const sdkInfo = results.map((r) => {
45352
- const parts = [];
45353
- if (r.experience_file_path) parts.push(`experience path ${r.experience_file_path}`);
45354
- if (r.ets_file_path) parts.push(`sample code path ${r.ets_file_path}`);
45355
- if (r.sdk_file_path) parts.push(`sdk info path ${r.sdk_file_path}`);
45356
- return parts.join("\n");
45357
- }).join("\n\n");
45358
- return `You can read the following files as needed to obtain information.
45359
- ${sdkInfo}`;
45360
- } catch (e) {
45361
- return `Error: ${e instanceof Error ? e.message : String(e)}`;
45362
- }
45363
- }
45364
- });
45365
- }
45366
-
45367
45336
  // src/tools/harmony-doc/api-client.ts
45368
45337
  var BASE_URL = "https://svc-drcn.developer.huawei.com/community/servlet";
45369
45338
  var HEADERS = {
45370
45339
  "Content-Type": "application/json",
45371
- "Referer": "https://developer.huawei.com/consumer/cn/doc/",
45340
+ Referer: "https://developer.huawei.com/consumer/cn/doc/",
45372
45341
  "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
45373
45342
  };
45343
+ var REQUEST_TIMEOUT_MS = 1e4;
45374
45344
  async function post(path7, body) {
45375
- const res = await fetch(BASE_URL + path7, {
45376
- method: "POST",
45377
- headers: HEADERS,
45378
- body: JSON.stringify(body)
45379
- });
45380
- if (!res.ok) {
45381
- throw new Error(`API request failed: ${res.status} ${res.statusText} for ${path7}`);
45382
- }
45383
- const data = await res.json();
45384
- if (data.code && data.code !== 0 && data.code !== "0") {
45385
- throw new Error(`API error [${data.code}]: ${data.message} for ${path7}`);
45345
+ const controller = new AbortController();
45346
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
45347
+ try {
45348
+ const res = await fetch(BASE_URL + path7, {
45349
+ method: "POST",
45350
+ headers: HEADERS,
45351
+ body: JSON.stringify(body),
45352
+ signal: controller.signal
45353
+ });
45354
+ if (!res.ok) {
45355
+ throw new Error(`API request failed: ${res.status} ${res.statusText} for ${path7}`);
45356
+ }
45357
+ const data = await res.json();
45358
+ if (data.code && data.code !== 0 && data.code !== "0") {
45359
+ throw new Error(`API error [${data.code}]: ${data.message} for ${path7}`);
45360
+ }
45361
+ return data;
45362
+ } finally {
45363
+ clearTimeout(timer);
45386
45364
  }
45387
- return data;
45365
+ }
45366
+ var CATALOG_DISPLAY_NAMES = {
45367
+ "harmonyos-guides": "\u6307\u5357",
45368
+ "harmonyos-references": "API\u53C2\u8003",
45369
+ "harmonyos-best-practices": "\u6700\u4F73\u5B9E\u8DF5",
45370
+ "harmonyos-faqs": "FAQ",
45371
+ "design-guides": "\u8BBE\u8BA1\u6307\u5357",
45372
+ "atomic-guides": "\u6307\u5357",
45373
+ "atomic-references": "API\u53C2\u8003",
45374
+ "atomic-faqs": "FAQ",
45375
+ "atomic-ascf": "ASCF\u6846\u67B6"
45376
+ };
45377
+ function catalogDisplayName(catalogName) {
45378
+ return CATALOG_DISPLAY_NAMES[catalogName] || "";
45388
45379
  }
45389
45380
  async function searchDocs(keyword, maxResults = 10) {
45390
45381
  const data = await post(
@@ -45405,12 +45396,29 @@ async function searchDocs(keyword, maxResults = 10) {
45405
45396
  }
45406
45397
  );
45407
45398
  return (data.resultList ?? []).map((r) => {
45399
+ let subsection = "";
45400
+ let anchorId = "";
45401
+ if (r.anchorHighlightInfo) {
45402
+ subsection = r.anchorHighlightInfo.replace(/<\/?em>/g, "");
45403
+ }
45408
45404
  let breadcrumb = [];
45409
45405
  try {
45410
45406
  const ext = typeof r.metaData?.ext === "string" ? JSON.parse(r.metaData.ext) : r.metaData?.ext ?? {};
45411
45407
  const nodeNames = Array.isArray(ext.nodeNames) ? ext.nodeNames : [];
45408
+ const catalogName = typeof ext.catalogName === "string" ? ext.catalogName : "";
45412
45409
  if (nodeNames.length > 0) {
45413
- breadcrumb = nodeNames;
45410
+ const rootName = catalogDisplayName(catalogName) || catalogName;
45411
+ breadcrumb = rootName ? [rootName, ...nodeNames] : nodeNames;
45412
+ }
45413
+ if (subsection) {
45414
+ try {
45415
+ const anchors = typeof ext.anchorList === "string" ? JSON.parse(ext.anchorList) : ext.anchorList;
45416
+ if (Array.isArray(anchors)) {
45417
+ const match = anchors.find((a) => a.title === subsection);
45418
+ if (match) anchorId = match.anchorId ?? "";
45419
+ }
45420
+ } catch {
45421
+ }
45414
45422
  }
45415
45423
  } catch {
45416
45424
  }
@@ -45418,17 +45426,34 @@ async function searchDocs(keyword, maxResults = 10) {
45418
45426
  title: r.name ?? "",
45419
45427
  url: r.url ?? "",
45420
45428
  excerpt: (r.content ?? "").replace(/\n/g, " ").substring(0, 200),
45429
+ excerptTruncated: (r.content ?? "").replace(/\n/g, " ").length > 200,
45421
45430
  breadcrumb,
45422
45431
  contentType: r.metaData?.type ?? 0,
45423
- timestamp: r.metaData?.timestamp ?? ""
45432
+ timestamp: r.metaData?.timestamp ?? "",
45433
+ subsection,
45434
+ anchorId
45424
45435
  };
45425
45436
  });
45426
45437
  }
45427
45438
  function objectIdFromUrl(url3) {
45428
45439
  try {
45429
- const pathname = new URL(url3).pathname.replace(/\/$/, "");
45440
+ const parsed = new URL(url3);
45441
+ const pathname = parsed.pathname.replace(/\/$/, "");
45442
+ const queryId = parsed.searchParams.get("objectId") || parsed.searchParams.get("id");
45443
+ if (queryId) return queryId;
45430
45444
  const parts = pathname.split("/");
45431
- return parts[parts.length - 1] || null;
45445
+ const lastSegment = parts[parts.length - 1];
45446
+ if (lastSegment && lastSegment.length > 2) return lastSegment;
45447
+ if (parts.length >= 2) {
45448
+ const secondLast = parts[parts.length - 2];
45449
+ if (secondLast && secondLast.length > 2) return secondLast;
45450
+ }
45451
+ const hash3 = parsed.hash;
45452
+ if (hash3) {
45453
+ const hashId = hash3.replace(/^#/, "");
45454
+ if (hashId.length > 5) return hashId;
45455
+ }
45456
+ return null;
45432
45457
  } catch {
45433
45458
  return null;
45434
45459
  }
@@ -45442,48 +45467,148 @@ async function fetchDocument(objectId, language = "cn") {
45442
45467
  if (!v) {
45443
45468
  throw new Error(`Empty response for objectId: ${objectId}`);
45444
45469
  }
45470
+ const html = v.content?.content ?? "";
45445
45471
  return {
45446
45472
  title: v.title ?? "",
45447
- html: v.content?.content ?? "",
45473
+ html,
45448
45474
  objectId,
45449
45475
  catalogName: v.catalogName ?? "",
45450
45476
  displayUpdateTime: v.displayUpdateTime ?? ""
45451
45477
  };
45452
45478
  }
45479
+ var treeCache = /* @__PURE__ */ new Map();
45480
+ async function getCatalogTree(language, catalogName, objectId) {
45481
+ const body = { language, catalogName };
45482
+ if (objectId) body.objectId = objectId;
45483
+ const data = await post(
45484
+ "/consumer/cn/documentPortal/getCatalogTree",
45485
+ body
45486
+ );
45487
+ return data.value?.catalogTreeList ?? [];
45488
+ }
45489
+ async function resolveBreadcrumb(language, catalogName, relateDocument) {
45490
+ if (!relateDocument) return [];
45491
+ const cacheKey = `${language}:${catalogName}`;
45492
+ if (!treeCache.has(cacheKey)) {
45493
+ const nodes = await getCatalogTree(language, catalogName);
45494
+ const map4 = /* @__PURE__ */ new Map();
45495
+ const walk = (list) => {
45496
+ for (const node of list) {
45497
+ map4.set(node.nodeId, { name: node.nodeName, parent: node.parent, rel: node.relateDocument });
45498
+ if (node.children?.length) walk(node.children);
45499
+ }
45500
+ };
45501
+ walk(nodes);
45502
+ treeCache.set(cacheKey, map4);
45503
+ }
45504
+ const map3 = treeCache.get(cacheKey);
45505
+ let target;
45506
+ for (const node of map3.values()) {
45507
+ if (node.rel === relateDocument) {
45508
+ target = node;
45509
+ break;
45510
+ }
45511
+ }
45512
+ if (!target) return [];
45513
+ const breadcrumb = [];
45514
+ let current = target;
45515
+ while (current) {
45516
+ breadcrumb.unshift(current.name);
45517
+ current = current.parent ? map3.get(current.parent) : void 0;
45518
+ }
45519
+ const rootName = catalogDisplayName(catalogName);
45520
+ if (rootName && breadcrumb[0] !== rootName) {
45521
+ breadcrumb.unshift(rootName);
45522
+ }
45523
+ return breadcrumb;
45524
+ }
45525
+ function formatUpdateTime(utcTimeStr) {
45526
+ if (!utcTimeStr) return "";
45527
+ const dt = /* @__PURE__ */ new Date(utcTimeStr.replace(" ", "T") + "Z");
45528
+ if (isNaN(dt.getTime())) return utcTimeStr;
45529
+ const bj = new Date(dt.getTime() + 8 * 60 * 60 * 1e3);
45530
+ const y = bj.getUTCFullYear();
45531
+ const m = String(bj.getUTCMonth() + 1).padStart(2, "0");
45532
+ const d = String(bj.getUTCDate()).padStart(2, "0");
45533
+ const h = String(bj.getUTCHours()).padStart(2, "0");
45534
+ const min = String(bj.getUTCMinutes()).padStart(2, "0");
45535
+ return `${y}-${m}-${d} ${h}:${min}`;
45536
+ }
45453
45537
 
45454
- // src/tools/harmony-doc/doc-search-tool.ts
45455
- function docSearchTool(_managers) {
45538
+ // src/tools/skill-search/skill-search-tool.ts
45539
+ function skillSearchTool(managers) {
45456
45540
  return tool({
45457
- description: "Search HarmonyOS developer documentation by keyword. Results cover official docs, API references, guides, and community content from developer.huawei.com. The breadcrumb path in each result helps identify the source type \u2014 prefer official docs over community posts when accuracy is critical. HarmonyOS APIs evolve rapidly; prefer searching for the latest information. After finding relevant results, use the harmony-doc-view tool with the URL to read the full document.",
45541
+ description: "Search HarmonyOS development knowledge across BOTH local skill documents AND official Huawei documentation. Returns two sections: (1) Local Skill Results \u2014 file paths to curated code examples, best practices, and SDK API info from the harmonyos-atomic-dev skill directory; (2) Official Documentation \u2014 search results from developer.huawei.com with titles, URLs, breadcrumbs, and excerpts. Use this tool for any HarmonyOS API, component, Kit, or development pattern lookup. After reviewing results, read local skill files for code patterns and use harmony-doc-view to read full official documents by URL. IMPORTANT: This tool already searches official documentation \u2014 do NOT also call harmony-doc-view for the same purpose. Only use harmony-doc-view to read a specific document page from the URLs listed below. Each scenario only supports ONE tool call. Do NOT call this tool multiple times.",
45458
45542
  args: {
45459
- query: tool.schema.string("Search keyword or phrase. Use specific terms for best results, e.g. 'Button component', 'router navigation', 'NetworkRequest'."),
45460
- maxResults: tool.schema.number("Maximum number of search results to return.").min(1).max(20).default(10)
45543
+ skill_path: tool.schema.string("Absolute path to the harmonyos-atomic-dev skill directory. IMPORTANT: this path should not contain any prefix or suffix like 'file://' or 'SKILL.md'."),
45544
+ query: tool.schema.string("A decomposed requirement or intent describing what you want to find, broken down into searchable keywords separated by spaces. QUERY TIPS: For best results, decompose your intent into short space-separated keywords instead of long sentences. Include: 1) Kit or component name (e.g. ScanKit, AdsKit, ShareKit), 2) specific API or method names (e.g. scanBarcode, loadAd, ShareController), 3) feature keywords (e.g. \u626B\u7801, \u5E7F\u544A\u52A0\u8F7D, \u5206\u4EAB). Example: 'ScanKit \u626B\u7801 scanBarcode startScanForResult' instead of '\u5E2E\u6211\u5B9E\u73B0\u4E00\u4E2A\u626B\u7801\u529F\u80FD'. "),
45545
+ topK: tool.schema.number("Maximum number of top-ranked documents to return. Actual results may be fewer depending on query relevance.").min(1).max(3).default(3)
45461
45546
  },
45462
- execute: async (args, _context) => {
45463
- try {
45464
- log("[harmony-doc-search]", { query: args.query, maxResults: args.maxResults });
45465
- const results = await searchDocs(args.query, args.maxResults);
45466
- if (results.length === 0) {
45467
- return `No results found for "${args.query}". Try different keywords.`;
45468
- }
45469
- const formatted = results.map((r, i) => {
45470
- const parts = [];
45471
- parts.push(`${i + 1}. **${r.title}**`);
45472
- if (r.breadcrumb.length > 0) {
45473
- parts.push(` Path: ${r.breadcrumb.join(" > ")}`);
45474
- }
45475
- parts.push(` URL: ${r.url}`);
45547
+ execute: async (args, context) => {
45548
+ const [localResults, docResults] = await Promise.all([
45549
+ searchSkill(args.skill_path, args.query, args.topK).catch(
45550
+ (e) => new Error(`Local search failed: ${e instanceof Error ? e.message : String(e)}`)
45551
+ ),
45552
+ searchDocs(args.query, 5).catch(
45553
+ (e) => new Error(`Doc search failed: ${e instanceof Error ? e.message : String(e)}`)
45554
+ )
45555
+ ]);
45556
+ const parts = [];
45557
+ if (localResults instanceof Error) {
45558
+ parts.push(`## Local Skill Results
45559
+
45560
+ ${localResults.message}`);
45561
+ } else if (localResults.length === 0) {
45562
+ parts.push("## Local Skill Results\n\nNo matching skill documents found.");
45563
+ } else {
45564
+ const localInfo = localResults.map((r) => {
45565
+ const lines = [];
45566
+ if (r.experience_file_path) lines.push(`- experience path: ${r.experience_file_path}`);
45567
+ if (r.ets_file_path) lines.push(`- sample code path: ${r.ets_file_path}`);
45568
+ if (r.sdk_file_path) lines.push(`- sdk info path: ${r.sdk_file_path}`);
45569
+ return lines.join("\n");
45570
+ }).join("\n\n");
45571
+ parts.push(`## Local Skill Results
45572
+
45573
+ ${localInfo}`);
45574
+ }
45575
+ if (docResults instanceof Error) {
45576
+ parts.push(`## Official Documentation
45577
+
45578
+ ${docResults.message}`);
45579
+ } else if (docResults.length === 0) {
45580
+ parts.push("## Official Documentation\n\nNo matching official documents found.");
45581
+ } else {
45582
+ const docInfo = docResults.map((r, i) => {
45583
+ const lines = [];
45584
+ let title = r.title;
45585
+ if (r.subsection && r.subsection !== r.title) {
45586
+ title += ` #${r.subsection}`;
45587
+ }
45588
+ lines.push(`${i + 1}. **${title}**`);
45589
+ let displayUrl = r.url;
45590
+ if (r.anchorId) {
45591
+ displayUrl += `#${r.anchorId}`;
45592
+ }
45593
+ lines.push(` URL: ${displayUrl}`);
45476
45594
  if (r.excerpt) {
45477
- parts.push(` ${r.excerpt}`);
45595
+ const suffix = r.excerptTruncated ? "..." : "";
45596
+ lines.push(` ${r.excerpt}${suffix}`);
45597
+ }
45598
+ if (r.breadcrumb.length > 0) {
45599
+ lines.push(` \u6765\u81EA: ${r.breadcrumb.join(" > ")}`);
45478
45600
  }
45479
- return parts.join("\n");
45601
+ return lines.join("\n");
45480
45602
  }).join("\n\n");
45481
- return `Found ${results.length} results for "${args.query}":
45603
+ parts.push(
45604
+ `## Official Documentation
45482
45605
 
45483
- ${formatted}`;
45484
- } catch (e) {
45485
- return `Error: ${e instanceof Error ? e.message : String(e)}`;
45606
+ ${docInfo}
45607
+
45608
+ Use harmony-doc-view with a URL above to read any official document in full.`
45609
+ );
45486
45610
  }
45611
+ return parts.join("\n\n");
45487
45612
  }
45488
45613
  });
45489
45614
  }
@@ -46176,18 +46301,61 @@ var removeUIParagraphs = {
46176
46301
  return "";
46177
46302
  }
46178
46303
  };
46304
+ var NAV_SHORT_TEXT = /* @__PURE__ */ new Set([
46305
+ "\u7B80\u4F53\u4E2D\u6587",
46306
+ "English",
46307
+ "\u4E0B\u8F7D App",
46308
+ "\u63A2\u7D22",
46309
+ "\u8BBE\u8BA1",
46310
+ "\u5F00\u53D1",
46311
+ "\u5206\u53D1",
46312
+ "\u63A8\u5E7F\u4E0E\u53D8\u73B0",
46313
+ "\u751F\u6001\u5408\u4F5C",
46314
+ "\u652F\u6301",
46315
+ "\u66F4\u591A",
46316
+ "\u7ACB\u5373\u767B\u5F55",
46317
+ "\u8F93\u5165\u5173\u952E\u5B57\u641C\u7D22",
46318
+ "Hello\uFF0C",
46319
+ "\u6B22\u8FCE\u6765\u5230\u5F00\u53D1\u8005\u8054\u76DF",
46320
+ "CTRL+K",
46321
+ "Created with Pixso",
46322
+ "\u7248\u672C\u8BF4\u660E",
46323
+ "\u6307\u5357",
46324
+ "API\u53C2\u8003",
46325
+ "\u6700\u4F73\u5B9E\u8DF5",
46326
+ "FAQ",
46327
+ "\u53D8\u66F4\u9884\u544A",
46328
+ "\u591A\u8BBE\u5907\u573A\u666F",
46329
+ "\u624B\u673A",
46330
+ "\u5E94\u7528\u8D28\u91CF",
46331
+ "\u6280\u672F\u8D28\u91CF",
46332
+ "\u5F00\u53D1\u8005\u80FD\u529B\u8BA4\u8BC1",
46333
+ "\u6211\u7684",
46334
+ "\u7BA1\u7406\u4E2D\u5FC3",
46335
+ "\u4E2A\u4EBA\u4E2D\u5FC3",
46336
+ "\u6211\u7684\u5B66\u5802",
46337
+ "\u6211\u7684\u6536\u85CF",
46338
+ "\u6211\u7684\u6D3B\u52A8",
46339
+ "\u6211\u7684\u5DE5\u5355"
46340
+ ]);
46179
46341
  var removeNavigation = {
46180
46342
  filter(node) {
46181
46343
  if (node.nodeType !== 1) return false;
46344
+ const text = (node.textContent ?? "").trim();
46345
+ if (text.length <= 20 && NAV_SHORT_TEXT.has(text)) {
46346
+ return true;
46347
+ }
46348
+ if (node.nodeName === "P" && text.length === 0) {
46349
+ const hasContent = node.querySelector("img, code, pre, table");
46350
+ if (!hasContent) return true;
46351
+ }
46182
46352
  const cls = node.className?.toLowerCase() ?? "";
46183
46353
  const id = node.id?.toLowerCase() ?? "";
46184
46354
  const unwantedClasses = [
46185
46355
  "top-bar",
46186
46356
  "menu-bar",
46187
- "sidebar",
46188
46357
  "search-bar",
46189
46358
  "breadcrumb",
46190
- "toolbar",
46191
46359
  "footer-nav",
46192
46360
  "main-nav",
46193
46361
  "side-nav",
@@ -46204,7 +46372,29 @@ var removeNavigation = {
46204
46372
  "feedback-section",
46205
46373
  "qrcode-section"
46206
46374
  ];
46207
- return unwantedClasses.some((c) => cls.includes(c) || id.includes(c));
46375
+ const classList = cls.split(/\s+/);
46376
+ const matchesClass = classList.some(
46377
+ (c) => unwantedClasses.some((u) => c === u || c.startsWith(u + "-") || c.endsWith("-" + u))
46378
+ );
46379
+ const matchesId = unwantedClasses.some(
46380
+ (u) => id === u || id.startsWith(u + "-") || id.endsWith("-" + u)
46381
+ );
46382
+ return matchesClass || matchesId;
46383
+ },
46384
+ replacement() {
46385
+ return "";
46386
+ }
46387
+ };
46388
+ var removeCodeUI = {
46389
+ filter(node) {
46390
+ if (node.nodeType === 3) {
46391
+ return isUnwantedText(node.textContent ?? "");
46392
+ }
46393
+ if (node.nodeType === 1 && ["P", "DIV", "SPAN"].includes(node.nodeName)) {
46394
+ const text = (node.textContent ?? "").trim();
46395
+ return isUnwantedText(text);
46396
+ }
46397
+ return false;
46208
46398
  },
46209
46399
  replacement() {
46210
46400
  return "";
@@ -46240,6 +46430,18 @@ var huaweiCodeBlock = {
46240
46430
  return "\n```" + language + "\n" + lines.join("\n") + "\n```\n";
46241
46431
  }
46242
46432
  };
46433
+ function inferLanguage(codeText) {
46434
+ if (codeText.includes("import ") || codeText.includes("export ") || codeText.includes("interface ")) {
46435
+ return "typescript";
46436
+ }
46437
+ if (codeText.includes("#include") || codeText.includes("int main")) {
46438
+ return "cpp";
46439
+ }
46440
+ if (codeText.includes("public class")) {
46441
+ return "java";
46442
+ }
46443
+ return "";
46444
+ }
46243
46445
  var genericPre = {
46244
46446
  filter(node) {
46245
46447
  return node.nodeName === "PRE" && !node.querySelector("ol.linenums");
@@ -46257,6 +46459,9 @@ var genericPre = {
46257
46459
  const codeMatch = (codeElem.className ?? "").match(/language-(\w+)/);
46258
46460
  if (codeMatch) language = codeMatch[1];
46259
46461
  }
46462
+ if (!language && codeText) {
46463
+ language = inferLanguage(codeText);
46464
+ }
46260
46465
  const cleanLines = codeText.split("\n").filter((line) => {
46261
46466
  const trimmed = line.trim();
46262
46467
  return !trimmed || !isUnwantedText(trimmed);
@@ -46264,6 +46469,30 @@ var genericPre = {
46264
46469
  return "\n```" + language + "\n" + cleanLines.join("\n") + "\n```\n";
46265
46470
  }
46266
46471
  };
46472
+ var standardCodeBlock = {
46473
+ filter(node) {
46474
+ return node.nodeName === "PRE" && !!node.firstChild && node.firstChild.nodeName === "CODE";
46475
+ },
46476
+ replacement(content, node) {
46477
+ const codeNode = node.firstChild;
46478
+ const lang = codeNode.className || codeNode.getAttribute("class") || "";
46479
+ const langMatch = lang.match(/language-(\w+)|hljs language-(\w+)/);
46480
+ const language = langMatch ? langMatch[1] || langMatch[2] : "";
46481
+ const codeContent = codeNode.textContent || content;
46482
+ const cleanContent = codeContent.split("\n").filter((line) => {
46483
+ const trimmed = line.trim();
46484
+ return !isUnwantedText(trimmed);
46485
+ }).join("\n");
46486
+ return "\n```" + language + "\n" + cleanContent + "\n```\n";
46487
+ }
46488
+ };
46489
+ function cleanCellText(text) {
46490
+ let cleaned = text.replace(/\s+/g, " ");
46491
+ for (const t of UI_TEXT) {
46492
+ cleaned = cleaned.replaceAll(t, "");
46493
+ }
46494
+ return cleaned.trim();
46495
+ }
46267
46496
  var harmonyTable = {
46268
46497
  filter(node) {
46269
46498
  return node.nodeName === "TABLE";
@@ -46282,7 +46511,7 @@ var harmonyTable = {
46282
46511
  }
46283
46512
  const code = cell.querySelector("code");
46284
46513
  if (code) return `\`${code.textContent?.trim() ?? ""}\``;
46285
- return (cell.textContent ?? "").replace(/\s+/g, " ").trim();
46514
+ return cleanCellText(cell.textContent ?? "");
46286
46515
  });
46287
46516
  if (cells.length === 0) return;
46288
46517
  md += "| " + cells.join(" | ") + " |\n";
@@ -46294,6 +46523,49 @@ var harmonyTable = {
46294
46523
  return md + "\n";
46295
46524
  }
46296
46525
  };
46526
+ var divTable = {
46527
+ filter(node) {
46528
+ if (node.nodeType !== 1 || node.nodeName !== "DIV") return false;
46529
+ const cls = (node.className ?? "").toLowerCase();
46530
+ const tableClasses = ["table", "tbl", "data-table", "table-container", "table-wrap"];
46531
+ if (!tableClasses.some((c) => cls.includes(c))) return false;
46532
+ const rowDivs = Array.from(node.children).filter((child) => {
46533
+ const childClass = (child.className ?? "").toLowerCase();
46534
+ return childClass.includes("tr") || childClass.includes("row") || childClass.includes("table-row");
46535
+ });
46536
+ return rowDivs.length > 0;
46537
+ },
46538
+ replacement(content, node) {
46539
+ const rows = Array.from(node.children).filter((child) => {
46540
+ const childClass = (child.className ?? "").toLowerCase();
46541
+ return childClass.includes("tr") || childClass.includes("row") || childClass.includes("table-row");
46542
+ });
46543
+ if (rows.length === 0) return content;
46544
+ let md = "\n";
46545
+ rows.forEach((row, rowIndex) => {
46546
+ const cells = Array.from(row.children).filter((child) => {
46547
+ const childClass = (child.className ?? "").toLowerCase();
46548
+ return childClass.includes("td") || childClass.includes("th") || childClass.includes("cell");
46549
+ }).map((cell) => {
46550
+ const link = cell.querySelector("a");
46551
+ if (link?.getAttribute("href")) {
46552
+ const href = link.getAttribute("href");
46553
+ const full = href.startsWith("http") ? href : `https://developer.huawei.com${href}`;
46554
+ return `[${link.textContent?.trim() ?? ""}](${full})`;
46555
+ }
46556
+ const code = cell.querySelector("code");
46557
+ if (code) return `\`${code.textContent?.trim() ?? ""}\``;
46558
+ return cleanCellText(cell.textContent ?? "");
46559
+ });
46560
+ if (cells.length === 0) return;
46561
+ md += "| " + cells.join(" | ") + " |\n";
46562
+ if (rowIndex === 0) {
46563
+ md += "| " + cells.map(() => "---").join(" | ") + " |\n";
46564
+ }
46565
+ });
46566
+ return md + "\n";
46567
+ }
46568
+ };
46297
46569
  var processLinks = {
46298
46570
  filter(node) {
46299
46571
  return node.nodeName === "A" && !!node.getAttribute("href");
@@ -46310,10 +46582,13 @@ function addCustomRules(td) {
46310
46582
  ["removeCodeUIContainers", removeCodeUIContainers],
46311
46583
  ["removeUIParagraphs", removeUIParagraphs],
46312
46584
  ["removeNavigation", removeNavigation],
46585
+ ["removeCodeUI", removeCodeUI],
46313
46586
  ["removePlatformBadge", removePlatformBadge],
46314
46587
  ["huaweiCodeBlock", huaweiCodeBlock],
46315
46588
  ["genericPre", genericPre],
46589
+ ["standardCodeBlock", standardCodeBlock],
46316
46590
  ["harmonyTable", harmonyTable],
46591
+ ["divTable", divTable],
46317
46592
  ["processLinks", processLinks]
46318
46593
  ];
46319
46594
  for (const [name, rule] of rules2) {
@@ -46388,7 +46663,7 @@ function cleanupMarkdown(md) {
46388
46663
  return out.trim();
46389
46664
  }
46390
46665
 
46391
- // src/tools/harmony-doc/doc-view-tool.ts
46666
+ // src/tools/harmony-doc/harmony-doc-tool.ts
46392
46667
  var docCache = /* @__PURE__ */ new Map();
46393
46668
  var CACHE_TTL = 10 * 60 * 1e3;
46394
46669
  function getCached(objectId) {
@@ -46400,71 +46675,110 @@ function getCached(objectId) {
46400
46675
  }
46401
46676
  return entry;
46402
46677
  }
46403
- function setCache(objectId, doc, markdown) {
46404
- docCache.set(objectId, { doc, markdown, fetchedAt: Date.now() });
46678
+ function setCache(objectId, doc, markdown, breadcrumb) {
46679
+ docCache.set(objectId, { doc, markdown, breadcrumb, fetchedAt: Date.now() });
46405
46680
  }
46406
- function docViewTool(_managers) {
46681
+ function harmonyDocViewTool(_managers) {
46407
46682
  return tool({
46408
- description: "Fetch and display the content of a HarmonyOS developer document by its URL. Use this tool after harmony-doc-search to read the full content of a relevant document. The URL should be from developer.huawei.com (e.g. from search results). For long documents, use the page parameter to read content in chunks.",
46683
+ description: "View a HarmonyOS official documentation page by URL with full Markdown content and pagination. Use this tool to read official API references, guides, and best practices from developer.huawei.com. Typically used with URLs obtained from skillSearch's Official Documentation results. Supports pagination for long documents \u2014 use the 'page' parameter to continue reading.",
46409
46684
  args: {
46410
- url: tool.schema.string("Full URL of the HarmonyOS document to view. Must be a developer.huawei.com document URL."),
46411
- page: tool.schema.number("Page number for long documents. Starts at 1. Use this when the response indicates more pages are available.").min(1).default(1)
46685
+ url: tool.schema.string("Full URL of the document to view. Must be a developer.huawei.com URL, typically from skillSearch results."),
46686
+ page: tool.schema.number("Page number for long documents. Starts at 1.").min(1).default(1)
46412
46687
  },
46413
46688
  execute: async (args, _context) => {
46414
- try {
46415
- log("[harmony-doc-view]", { url: args.url, page: args.page });
46416
- const objectId = objectIdFromUrl(args.url);
46417
- if (!objectId) {
46418
- return `Error: Cannot extract document ID from URL: ${args.url}`;
46419
- }
46420
- let cached3 = getCached(objectId);
46421
- let doc;
46422
- let markdown;
46423
- if (cached3) {
46424
- doc = cached3.doc;
46425
- markdown = cached3.markdown;
46426
- log("[harmony-doc-view] cache hit", { objectId, page: args.page });
46427
- } else {
46428
- doc = await fetchDocument(objectId);
46429
- if (!doc.html) {
46430
- return `Error: Document content is empty for ${args.url}`;
46431
- }
46432
- markdown = htmlToMarkdown(doc.html);
46433
- setCache(objectId, doc, markdown);
46434
- log("[harmony-doc-view] cache miss, fetched from network", { objectId });
46435
- }
46436
- const MAX_LENGTH = 3e4;
46437
- const totalPages = Math.max(1, Math.ceil(markdown.length / MAX_LENGTH));
46438
- const page = Math.min(args.page, totalPages);
46439
- const start = (page - 1) * MAX_LENGTH;
46440
- const end = Math.min(page * MAX_LENGTH, markdown.length);
46441
- const chunk = markdown.substring(start, end);
46442
- let output = `# ${doc.title}
46443
-
46444
- Source: ${args.url}
46689
+ return executeView(args.url, args.page);
46690
+ }
46691
+ });
46692
+ }
46693
+ async function executeView(url3, page) {
46694
+ try {
46695
+ log("[harmony-doc-view]", { url: url3, page });
46696
+ const objectId = objectIdFromUrl(url3);
46697
+ if (!objectId) {
46698
+ return `Error: Cannot extract document ID from URL: ${url3}`;
46699
+ }
46700
+ let cached3 = getCached(objectId);
46701
+ let doc;
46702
+ let markdown;
46703
+ let breadcrumb;
46704
+ if (cached3) {
46705
+ doc = cached3.doc;
46706
+ markdown = cached3.markdown;
46707
+ breadcrumb = cached3.breadcrumb;
46708
+ log("[harmony-doc-view] cache hit", { objectId, page });
46709
+ } else {
46710
+ doc = await fetchDocument(objectId);
46711
+ if (!doc.html) {
46712
+ log("[harmony-doc-view] API returned empty HTML", { objectId, title: doc.title });
46713
+ return `Error: Document content is empty for ${url3} (objectId: ${objectId}). The API returned no HTML content \u2014 the document may not be available through this API.`;
46714
+ }
46715
+ markdown = htmlToMarkdown(doc.html);
46716
+ log("[harmony-doc-view] conversion result", {
46717
+ objectId,
46718
+ htmlLength: doc.html.length,
46719
+ markdownLength: markdown.length,
46720
+ markdownPreview: markdown.substring(0, 300)
46721
+ });
46722
+ if (!markdown.trim()) {
46723
+ log("[harmony-doc-view] markdown conversion produced empty output", {
46724
+ objectId,
46725
+ htmlLength: doc.html.length
46726
+ });
46727
+ return `Error: Document HTML was non-empty (${doc.html.length} chars) but converted to empty markdown for ${url3}. This may indicate the HTML structure is not supported.`;
46728
+ }
46729
+ breadcrumb = [];
46730
+ if (doc.catalogName) {
46731
+ try {
46732
+ breadcrumb = await resolveBreadcrumb("cn", doc.catalogName, objectId);
46733
+ } catch (e) {
46734
+ log("[harmony-doc-view] breadcrumb resolution failed", { objectId, error: String(e) });
46735
+ }
46736
+ }
46737
+ setCache(objectId, doc, markdown, breadcrumb);
46738
+ log("[harmony-doc-view] cache miss, fetched from network", { objectId });
46739
+ }
46740
+ const MAX_LENGTH = 3e4;
46741
+ const totalPages = Math.max(1, Math.ceil(markdown.length / MAX_LENGTH));
46742
+ const currentPage = Math.min(page ?? 1, totalPages);
46743
+ const start = (currentPage - 1) * MAX_LENGTH;
46744
+ const end = Math.min(currentPage * MAX_LENGTH, markdown.length);
46745
+ const chunk = markdown.substring(start, end);
46746
+ let output = `# ${doc.title}
46747
+
46748
+ Source: ${url3}
46445
46749
  `;
46446
- if (doc.displayUpdateTime) {
46447
- output += `Updated: ${doc.displayUpdateTime}
46750
+ if (breadcrumb.length > 0) {
46751
+ output += `Path: ${breadcrumb.join(" > ")}
46448
46752
  `;
46449
- }
46450
- if (totalPages > 1) {
46451
- output += `Page ${page} of ${totalPages}
46753
+ }
46754
+ const updateTime = formatUpdateTime(doc.displayUpdateTime);
46755
+ if (updateTime) {
46756
+ output += `Updated: ${updateTime}
46452
46757
  `;
46453
- }
46454
- output += "\n---\n\n";
46455
- output += chunk;
46456
- if (page < totalPages) {
46457
- output += `
46758
+ }
46759
+ if (totalPages > 1) {
46760
+ output += `Page ${currentPage} of ${totalPages}
46761
+ `;
46762
+ }
46763
+ output += "\n---\n\n";
46764
+ output += chunk;
46765
+ if (currentPage < totalPages) {
46766
+ output += `
46458
46767
 
46459
46768
  ---
46460
- [Document continues \u2014 call harmony-doc-view with page=${page + 1} to read more]`;
46461
- }
46462
- return output;
46463
- } catch (e) {
46464
- return `Error: ${e instanceof Error ? e.message : String(e)}`;
46465
- }
46466
- }
46467
- });
46769
+ [Document continues \u2014 call with page=${currentPage + 1} to read more]`;
46770
+ }
46771
+ log("[harmony-doc-view] RETURN", {
46772
+ objectId,
46773
+ outputLength: output.length,
46774
+ chunkLength: chunk.length,
46775
+ markdownLength: markdown.length,
46776
+ outputEnd: output.substring(Math.max(0, output.length - 200))
46777
+ });
46778
+ return output;
46779
+ } catch (e) {
46780
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
46781
+ }
46468
46782
  }
46469
46783
 
46470
46784
  // src/tools/builtin.ts
@@ -46472,8 +46786,7 @@ function createBuiltinTools(managers) {
46472
46786
  return {
46473
46787
  createHmTemplate: createHmTemplateTool(managers),
46474
46788
  skillSearch: skillSearchTool(managers),
46475
- "harmony-doc-search": docSearchTool(managers),
46476
- "harmony-doc-view": docViewTool(managers)
46789
+ "harmony-doc-view": harmonyDocViewTool(managers)
46477
46790
  };
46478
46791
  }
46479
46792
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@harmonyos-arkts/opencode-plugin",
4
- "version": "0.0.10",
4
+ "version": "0.0.12",
5
5
  "description": "HarmonyOS Full-Lifecycle Development Assistant. Specialized in the complete development lifecycle of HarmonyOS applications, including project creation, UI development, state management, network requests, data storage, permission requests, performance optimization, testing, and release.",
6
6
  "type": "module",
7
7
  "license": "MIT",