@harmonyos-arkts/opencode-plugin 0.0.11 → 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.
- package/dist/index.js +112 -130
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -45333,37 +45333,6 @@ 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 = {
|
|
@@ -45371,46 +45340,28 @@ var HEADERS = {
|
|
|
45371
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
|
};
|
|
45374
|
-
var
|
|
45375
|
-
var RETRY_DELAY_MS = 1e3;
|
|
45376
|
-
async function sleep(ms) {
|
|
45377
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
45378
|
-
}
|
|
45343
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
45379
45344
|
async function post(path7, body) {
|
|
45380
|
-
|
|
45381
|
-
|
|
45382
|
-
|
|
45383
|
-
|
|
45384
|
-
|
|
45385
|
-
|
|
45386
|
-
|
|
45387
|
-
|
|
45388
|
-
|
|
45389
|
-
|
|
45390
|
-
|
|
45391
|
-
|
|
45392
|
-
|
|
45393
|
-
|
|
45394
|
-
|
|
45395
|
-
throw new Error(`API request failed: ${res.status} ${res.statusText} for ${path7}`);
|
|
45396
|
-
}
|
|
45397
|
-
const data = await res.json();
|
|
45398
|
-
if (data.code && data.code !== 0 && data.code !== "0") {
|
|
45399
|
-
throw new Error(`API error [${data.code}]: ${data.message} for ${path7}`);
|
|
45400
|
-
}
|
|
45401
|
-
return data;
|
|
45402
|
-
} catch (e) {
|
|
45403
|
-
if (e instanceof TypeError && e.message.includes("fetch")) {
|
|
45404
|
-
lastError = e;
|
|
45405
|
-
if (attempt < MAX_RETRIES) {
|
|
45406
|
-
await sleep(RETRY_DELAY_MS * (attempt + 1));
|
|
45407
|
-
continue;
|
|
45408
|
-
}
|
|
45409
|
-
}
|
|
45410
|
-
throw e;
|
|
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}`);
|
|
45411
45360
|
}
|
|
45361
|
+
return data;
|
|
45362
|
+
} finally {
|
|
45363
|
+
clearTimeout(timer);
|
|
45412
45364
|
}
|
|
45413
|
-
throw lastError ?? new Error("Request failed after retries");
|
|
45414
45365
|
}
|
|
45415
45366
|
var CATALOG_DISPLAY_NAMES = {
|
|
45416
45367
|
"harmonyos-guides": "\u6307\u5357",
|
|
@@ -45475,6 +45426,7 @@ async function searchDocs(keyword, maxResults = 10) {
|
|
|
45475
45426
|
title: r.name ?? "",
|
|
45476
45427
|
url: r.url ?? "",
|
|
45477
45428
|
excerpt: (r.content ?? "").replace(/\n/g, " ").substring(0, 200),
|
|
45429
|
+
excerptTruncated: (r.content ?? "").replace(/\n/g, " ").length > 200,
|
|
45478
45430
|
breadcrumb,
|
|
45479
45431
|
contentType: r.metaData?.type ?? 0,
|
|
45480
45432
|
timestamp: r.metaData?.timestamp ?? "",
|
|
@@ -45583,6 +45535,84 @@ function formatUpdateTime(utcTimeStr) {
|
|
|
45583
45535
|
return `${y}-${m}-${d} ${h}:${min}`;
|
|
45584
45536
|
}
|
|
45585
45537
|
|
|
45538
|
+
// src/tools/skill-search/skill-search-tool.ts
|
|
45539
|
+
function skillSearchTool(managers) {
|
|
45540
|
+
return tool({
|
|
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.",
|
|
45542
|
+
args: {
|
|
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)
|
|
45546
|
+
},
|
|
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}`);
|
|
45594
|
+
if (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(" > ")}`);
|
|
45600
|
+
}
|
|
45601
|
+
return lines.join("\n");
|
|
45602
|
+
}).join("\n\n");
|
|
45603
|
+
parts.push(
|
|
45604
|
+
`## Official Documentation
|
|
45605
|
+
|
|
45606
|
+
${docInfo}
|
|
45607
|
+
|
|
45608
|
+
Use harmony-doc-view with a URL above to read any official document in full.`
|
|
45609
|
+
);
|
|
45610
|
+
}
|
|
45611
|
+
return parts.join("\n\n");
|
|
45612
|
+
}
|
|
45613
|
+
});
|
|
45614
|
+
}
|
|
45615
|
+
|
|
45586
45616
|
// node_modules/turndown/lib/turndown.es.js
|
|
45587
45617
|
function extend3(destination) {
|
|
45588
45618
|
for (var i = 1; i < arguments.length; i++) {
|
|
@@ -46648,69 +46678,21 @@ function getCached(objectId) {
|
|
|
46648
46678
|
function setCache(objectId, doc, markdown, breadcrumb) {
|
|
46649
46679
|
docCache.set(objectId, { doc, markdown, breadcrumb, fetchedAt: Date.now() });
|
|
46650
46680
|
}
|
|
46651
|
-
function
|
|
46681
|
+
function harmonyDocViewTool(_managers) {
|
|
46652
46682
|
return tool({
|
|
46653
|
-
description: "HarmonyOS
|
|
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.",
|
|
46654
46684
|
args: {
|
|
46655
|
-
|
|
46656
|
-
|
|
46657
|
-
maxResults: tool.schema.number("Maximum search results to return (for action='search').").min(1).max(20).default(10),
|
|
46658
|
-
url: tool.schema.string("Full URL of the document to view (for action='view'). Must be a developer.huawei.com URL from search results.").optional(),
|
|
46659
|
-
page: tool.schema.number("Page number for long documents (for action='view'). Starts at 1.").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)
|
|
46660
46687
|
},
|
|
46661
46688
|
execute: async (args, _context) => {
|
|
46662
|
-
if (args.action === "search") {
|
|
46663
|
-
return executeSearch(args.query, args.maxResults);
|
|
46664
|
-
}
|
|
46665
46689
|
return executeView(args.url, args.page);
|
|
46666
46690
|
}
|
|
46667
46691
|
});
|
|
46668
46692
|
}
|
|
46669
|
-
async function executeSearch(query, maxResults) {
|
|
46670
|
-
if (!query) {
|
|
46671
|
-
return "Error: 'query' is required when action is 'search'.";
|
|
46672
|
-
}
|
|
46673
|
-
try {
|
|
46674
|
-
log("[harmony-doc search]", { query, maxResults });
|
|
46675
|
-
const results = await searchDocs(query, maxResults ?? 10);
|
|
46676
|
-
if (results.length === 0) {
|
|
46677
|
-
return `No results found for "${query}". Try different keywords.`;
|
|
46678
|
-
}
|
|
46679
|
-
const formatted = results.map((r, i) => {
|
|
46680
|
-
const parts = [];
|
|
46681
|
-
let title = r.title;
|
|
46682
|
-
if (r.subsection && r.subsection !== r.title) {
|
|
46683
|
-
title += ` #${r.subsection}`;
|
|
46684
|
-
}
|
|
46685
|
-
parts.push(`${i + 1}. **${title}**`);
|
|
46686
|
-
if (r.breadcrumb.length > 0) {
|
|
46687
|
-
parts.push(` Path: ${r.breadcrumb.join(" > ")}`);
|
|
46688
|
-
}
|
|
46689
|
-
let displayUrl = r.url;
|
|
46690
|
-
if (r.anchorId) {
|
|
46691
|
-
displayUrl += `#${r.anchorId}`;
|
|
46692
|
-
}
|
|
46693
|
-
parts.push(` URL: ${displayUrl}`);
|
|
46694
|
-
if (r.excerpt) {
|
|
46695
|
-
parts.push(` ${r.excerpt}`);
|
|
46696
|
-
}
|
|
46697
|
-
return parts.join("\n");
|
|
46698
|
-
}).join("\n\n");
|
|
46699
|
-
return `Found ${results.length} results for "${query}":
|
|
46700
|
-
|
|
46701
|
-
${formatted}
|
|
46702
|
-
|
|
46703
|
-
Use action='view' with the URL to read the full document.`;
|
|
46704
|
-
} catch (e) {
|
|
46705
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
46706
|
-
}
|
|
46707
|
-
}
|
|
46708
46693
|
async function executeView(url3, page) {
|
|
46709
|
-
if (!url3) {
|
|
46710
|
-
return "Error: 'url' is required when action is 'view'.";
|
|
46711
|
-
}
|
|
46712
46694
|
try {
|
|
46713
|
-
log("[harmony-doc
|
|
46695
|
+
log("[harmony-doc-view]", { url: url3, page });
|
|
46714
46696
|
const objectId = objectIdFromUrl(url3);
|
|
46715
46697
|
if (!objectId) {
|
|
46716
46698
|
return `Error: Cannot extract document ID from URL: ${url3}`;
|
|
@@ -46723,22 +46705,22 @@ async function executeView(url3, page) {
|
|
|
46723
46705
|
doc = cached3.doc;
|
|
46724
46706
|
markdown = cached3.markdown;
|
|
46725
46707
|
breadcrumb = cached3.breadcrumb;
|
|
46726
|
-
log("[harmony-doc
|
|
46708
|
+
log("[harmony-doc-view] cache hit", { objectId, page });
|
|
46727
46709
|
} else {
|
|
46728
46710
|
doc = await fetchDocument(objectId);
|
|
46729
46711
|
if (!doc.html) {
|
|
46730
|
-
log("[harmony-doc
|
|
46712
|
+
log("[harmony-doc-view] API returned empty HTML", { objectId, title: doc.title });
|
|
46731
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.`;
|
|
46732
46714
|
}
|
|
46733
46715
|
markdown = htmlToMarkdown(doc.html);
|
|
46734
|
-
log("[harmony-doc
|
|
46716
|
+
log("[harmony-doc-view] conversion result", {
|
|
46735
46717
|
objectId,
|
|
46736
46718
|
htmlLength: doc.html.length,
|
|
46737
46719
|
markdownLength: markdown.length,
|
|
46738
46720
|
markdownPreview: markdown.substring(0, 300)
|
|
46739
46721
|
});
|
|
46740
46722
|
if (!markdown.trim()) {
|
|
46741
|
-
log("[harmony-doc
|
|
46723
|
+
log("[harmony-doc-view] markdown conversion produced empty output", {
|
|
46742
46724
|
objectId,
|
|
46743
46725
|
htmlLength: doc.html.length
|
|
46744
46726
|
});
|
|
@@ -46749,11 +46731,11 @@ async function executeView(url3, page) {
|
|
|
46749
46731
|
try {
|
|
46750
46732
|
breadcrumb = await resolveBreadcrumb("cn", doc.catalogName, objectId);
|
|
46751
46733
|
} catch (e) {
|
|
46752
|
-
log("[harmony-doc
|
|
46734
|
+
log("[harmony-doc-view] breadcrumb resolution failed", { objectId, error: String(e) });
|
|
46753
46735
|
}
|
|
46754
46736
|
}
|
|
46755
46737
|
setCache(objectId, doc, markdown, breadcrumb);
|
|
46756
|
-
log("[harmony-doc
|
|
46738
|
+
log("[harmony-doc-view] cache miss, fetched from network", { objectId });
|
|
46757
46739
|
}
|
|
46758
46740
|
const MAX_LENGTH = 3e4;
|
|
46759
46741
|
const totalPages = Math.max(1, Math.ceil(markdown.length / MAX_LENGTH));
|
|
@@ -46784,9 +46766,9 @@ Source: ${url3}
|
|
|
46784
46766
|
output += `
|
|
46785
46767
|
|
|
46786
46768
|
---
|
|
46787
|
-
[Document continues \u2014 call with
|
|
46769
|
+
[Document continues \u2014 call with page=${currentPage + 1} to read more]`;
|
|
46788
46770
|
}
|
|
46789
|
-
log("[harmony-doc
|
|
46771
|
+
log("[harmony-doc-view] RETURN", {
|
|
46790
46772
|
objectId,
|
|
46791
46773
|
outputLength: output.length,
|
|
46792
46774
|
chunkLength: chunk.length,
|
|
@@ -46804,7 +46786,7 @@ function createBuiltinTools(managers) {
|
|
|
46804
46786
|
return {
|
|
46805
46787
|
createHmTemplate: createHmTemplateTool(managers),
|
|
46806
46788
|
skillSearch: skillSearchTool(managers),
|
|
46807
|
-
"harmony-doc":
|
|
46789
|
+
"harmony-doc-view": harmonyDocViewTool(managers)
|
|
46808
46790
|
};
|
|
46809
46791
|
}
|
|
46810
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.
|
|
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",
|