@hulistmi/hulistmi 1.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/LICENSE.md +22 -0
- package/README.md +289 -0
- package/bin/hulistmi.mjs +21 -0
- package/package.json +73 -0
- package/public/SKILL.md +26 -0
- package/public/_headers +3 -0
- package/public/favicon.svg +5 -0
- package/public/index.html +47 -0
- package/public/llms.txt +28 -0
- package/public/robots.txt +6 -0
- package/public/sitemap.xml +6 -0
- package/public/webmcp.js +3 -0
- package/src/cli.ts +67 -0
- package/src/index.ts +260 -0
- package/src/lib/catalog.ts +113 -0
- package/src/lib/cli-endpoints.ts +62 -0
- package/src/lib/documents.ts +164 -0
- package/src/lib/fetch.ts +165 -0
- package/src/lib/guides/fetch.ts +8 -0
- package/src/lib/guides/index.ts +3 -0
- package/src/lib/guides/render.ts +13 -0
- package/src/lib/guides/types.ts +5 -0
- package/src/lib/mcp.ts +119 -0
- package/src/lib/rate-limit.ts +27 -0
- package/src/lib/reference/fetch.ts +8 -0
- package/src/lib/reference/index.ts +3 -0
- package/src/lib/reference/render.ts +13 -0
- package/src/lib/reference/types.ts +5 -0
- package/src/lib/render.ts +216 -0
- package/src/lib/search.ts +81 -0
- package/src/lib/skill.ts +100 -0
- package/src/lib/types.ts +18 -0
- package/src/lib/upstream-contract.json +103 -0
- package/src/lib/upstream-contract.ts +3 -0
- package/src/lib/url.ts +50 -0
- package/src/lib/webmcp.ts +19 -0
- package/wrangler.jsonc +18 -0
package/src/lib/skill.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
|
+
|
|
3
|
+
export const SKILL_NAME = "hulistmi";
|
|
4
|
+
|
|
5
|
+
export const skillHeaders = {
|
|
6
|
+
"Access-Control-Allow-Origin": "*",
|
|
7
|
+
"Cache-Control": "public, max-age=300, s-maxage=600",
|
|
8
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const skillIndexHeaders = {
|
|
12
|
+
"Access-Control-Allow-Origin": "*",
|
|
13
|
+
"Cache-Control": "public, max-age=300, s-maxage=600",
|
|
14
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface SkillArtifact {
|
|
18
|
+
bytes: ArrayBuffer;
|
|
19
|
+
description: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function loadSkill(
|
|
24
|
+
assets: Fetcher,
|
|
25
|
+
baseUrl: string,
|
|
26
|
+
): Promise<SkillArtifact> {
|
|
27
|
+
const skillResponse = await assets.fetch(
|
|
28
|
+
new Request(new URL("/SKILL.md", baseUrl).toString()),
|
|
29
|
+
);
|
|
30
|
+
if (!skillResponse.ok)
|
|
31
|
+
throw new HTTPException(500, { message: "Failed to load SKILL.md" });
|
|
32
|
+
const bytes = await skillResponse.arrayBuffer();
|
|
33
|
+
const frontmatter = parseSkillFrontmatter(new TextDecoder().decode(bytes));
|
|
34
|
+
if (frontmatter.name !== SKILL_NAME)
|
|
35
|
+
throw new HTTPException(500, {
|
|
36
|
+
message: `Expected skill name "${SKILL_NAME}".`,
|
|
37
|
+
});
|
|
38
|
+
if (!frontmatter.description)
|
|
39
|
+
throw new HTTPException(500, { message: "Skill description is required." });
|
|
40
|
+
return {
|
|
41
|
+
bytes,
|
|
42
|
+
description: frontmatter.description,
|
|
43
|
+
name: frontmatter.name,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function skillExists(
|
|
48
|
+
assets: Fetcher,
|
|
49
|
+
baseUrl: string,
|
|
50
|
+
): Promise<boolean> {
|
|
51
|
+
const response = await assets.fetch(
|
|
52
|
+
new Request(new URL("/SKILL.md", baseUrl).toString(), { method: "HEAD" }),
|
|
53
|
+
);
|
|
54
|
+
return response.ok;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function createSkillIndex(skill: SkillArtifact) {
|
|
58
|
+
return {
|
|
59
|
+
$schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
|
60
|
+
skills: [
|
|
61
|
+
{
|
|
62
|
+
name: skill.name,
|
|
63
|
+
type: "skill-md",
|
|
64
|
+
description: skill.description,
|
|
65
|
+
url: `/.well-known/agent-skills/${skill.name}/SKILL.md`,
|
|
66
|
+
digest: `sha256:${await sha256Hex(skill.bytes)}`,
|
|
67
|
+
files: ["SKILL.md"],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseSkillFrontmatter(markdown: string): Record<string, string> {
|
|
74
|
+
const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
75
|
+
if (!match)
|
|
76
|
+
throw new HTTPException(500, {
|
|
77
|
+
message: "SKILL.md must start with YAML frontmatter.",
|
|
78
|
+
});
|
|
79
|
+
const fields: Record<string, string> = {};
|
|
80
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
81
|
+
if (!line.trim()) continue;
|
|
82
|
+
const separator = line.indexOf(":");
|
|
83
|
+
if (separator === -1)
|
|
84
|
+
throw new HTTPException(500, {
|
|
85
|
+
message: `Invalid SKILL.md frontmatter line: ${line}`,
|
|
86
|
+
});
|
|
87
|
+
fields[line.slice(0, separator).trim()] = line
|
|
88
|
+
.slice(separator + 1)
|
|
89
|
+
.trim()
|
|
90
|
+
.replace(/^['"]|['"]$/g, "");
|
|
91
|
+
}
|
|
92
|
+
return fields;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function sha256Hex(bytes: ArrayBuffer): Promise<string> {
|
|
96
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
97
|
+
return [...new Uint8Array(digest)]
|
|
98
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
99
|
+
.join("");
|
|
100
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface HarmonyContentBlock {
|
|
2
|
+
content: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface HarmonyDocumentValue {
|
|
6
|
+
status: string;
|
|
7
|
+
title: string;
|
|
8
|
+
content: HarmonyContentBlock | null;
|
|
9
|
+
anchorList?: Array<{ title?: string; anchorId?: string }>;
|
|
10
|
+
docId?: string | number;
|
|
11
|
+
updatedDate?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HarmonyDocumentResponse {
|
|
15
|
+
code: number | string;
|
|
16
|
+
message?: string;
|
|
17
|
+
value?: HarmonyDocumentValue;
|
|
18
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"allowedHosts": ["svc-drcn.developer.huawei.com"],
|
|
3
|
+
"allowedPaths": [
|
|
4
|
+
"/community/servlet/consumer/cn/documentPortal/getCatalogTree",
|
|
5
|
+
"/community/servlet/consumer/cn/documentPortal/getDocumentById",
|
|
6
|
+
"/community/servlet/consumer/cn/documentPortal/getNavigationAddress",
|
|
7
|
+
"/community/servlet/consumer/cn/documentPortal/checkCenterGrayUser",
|
|
8
|
+
"/community/servlet/consumer/cn/documentPortal/getCenterRootNodeTree",
|
|
9
|
+
"/community/servlet/consumer/cn/documentPortal/getCenterDocument",
|
|
10
|
+
"/community/servlet/consumer/partnerCommunityService/developer/search"
|
|
11
|
+
],
|
|
12
|
+
"catalogs": {
|
|
13
|
+
"harmonyos-guides": {
|
|
14
|
+
"request": {
|
|
15
|
+
"url": "https://svc-drcn.developer.huawei.com/community/servlet/consumer/cn/documentPortal/getCatalogTree",
|
|
16
|
+
"headers": {},
|
|
17
|
+
"body": {
|
|
18
|
+
"language": "en",
|
|
19
|
+
"catalogName": "harmonyos-guides",
|
|
20
|
+
"showHide": "1"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"responseHeaders": { "content-type": "application/json" }
|
|
24
|
+
},
|
|
25
|
+
"harmonyos-references": {
|
|
26
|
+
"request": {
|
|
27
|
+
"url": "https://svc-drcn.developer.huawei.com/community/servlet/consumer/cn/documentPortal/getCatalogTree",
|
|
28
|
+
"headers": {},
|
|
29
|
+
"body": {
|
|
30
|
+
"language": "en",
|
|
31
|
+
"catalogName": "harmonyos-references",
|
|
32
|
+
"showHide": "1"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"responseHeaders": { "content-type": "application/json" }
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"documents": {
|
|
39
|
+
"harmonyos-guides/start-overview": {
|
|
40
|
+
"checkCenterGrayUser": {
|
|
41
|
+
"url": "https://svc-drcn.developer.huawei.com/community/servlet/consumer/cn/documentPortal/checkCenterGrayUser",
|
|
42
|
+
"headers": {},
|
|
43
|
+
"body": {
|
|
44
|
+
"catalogName": "harmonyos-guides",
|
|
45
|
+
"language": "en",
|
|
46
|
+
"fileName": "start-overview",
|
|
47
|
+
"grayId": "11111111111111111111111111111111"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"getDocumentById": {
|
|
51
|
+
"url": "https://svc-drcn.developer.huawei.com/community/servlet/consumer/cn/documentPortal/getDocumentById",
|
|
52
|
+
"headers": {},
|
|
53
|
+
"body": {
|
|
54
|
+
"objectId": "start-overview",
|
|
55
|
+
"nodeAlias": null,
|
|
56
|
+
"catalogName": "harmonyos-guides",
|
|
57
|
+
"language": "en"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"harmonyos-references/js-apis-app-ability-uiability": {
|
|
62
|
+
"checkCenterGrayUser": {
|
|
63
|
+
"url": "https://svc-drcn.developer.huawei.com/community/servlet/consumer/cn/documentPortal/checkCenterGrayUser",
|
|
64
|
+
"headers": {},
|
|
65
|
+
"body": {
|
|
66
|
+
"catalogName": "harmonyos-references",
|
|
67
|
+
"language": "en",
|
|
68
|
+
"fileName": "js-apis-app-ability-uiability",
|
|
69
|
+
"grayId": "11111111111111111111111111111111"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"getDocumentById": {
|
|
73
|
+
"url": "https://svc-drcn.developer.huawei.com/community/servlet/consumer/cn/documentPortal/getDocumentById",
|
|
74
|
+
"headers": {},
|
|
75
|
+
"body": {
|
|
76
|
+
"objectId": "js-apis-app-ability-uiability",
|
|
77
|
+
"nodeAlias": null,
|
|
78
|
+
"catalogName": "harmonyos-references",
|
|
79
|
+
"language": "en"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"search": {
|
|
85
|
+
"url": "https://svc-drcn.developer.huawei.com/community/servlet/consumer/partnerCommunityService/developer/search",
|
|
86
|
+
"headers": {},
|
|
87
|
+
"responseHeaders": { "content-type": "application/json" },
|
|
88
|
+
"bodyForUIAbility": {
|
|
89
|
+
"deviceId": "ESN",
|
|
90
|
+
"deviceType": "1",
|
|
91
|
+
"language": "en",
|
|
92
|
+
"country": "CN",
|
|
93
|
+
"requestOrgin": 5,
|
|
94
|
+
"whetherToCorrect": 1,
|
|
95
|
+
"keyWord": "UIAbility",
|
|
96
|
+
"cutPage": { "offset": 0, "length": 20 },
|
|
97
|
+
"developerVertical": { "categoryList": [1], "language": "en" }
|
|
98
|
+
},
|
|
99
|
+
"maxQueryLength": 120,
|
|
100
|
+
"defaultLength": 20,
|
|
101
|
+
"maxLength": 20
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/lib/url.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const HUAWEI_DOC_ORIGIN = "https://developer.huawei.com";
|
|
2
|
+
const DOC_PREFIX = "/consumer/en/doc/";
|
|
3
|
+
|
|
4
|
+
export function normalizeDocsPath(path: string): string {
|
|
5
|
+
return path.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function generateHuaweiDocUrl(path: string): string {
|
|
9
|
+
return `${HUAWEI_DOC_ORIGIN}${DOC_PREFIX}${normalizeDocsPath(path)}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isValidHuaweiDocUrl(input: string): boolean {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(input);
|
|
15
|
+
return (
|
|
16
|
+
url.protocol === "https:" &&
|
|
17
|
+
url.hostname === "developer.huawei.com" &&
|
|
18
|
+
url.pathname.startsWith(DOC_PREFIX)
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function huaweiUrlToPath(input: string): string {
|
|
26
|
+
const url = new URL(input);
|
|
27
|
+
if (!url.pathname.startsWith(DOC_PREFIX))
|
|
28
|
+
throw new Error(`Unsupported Huawei documentation URL: ${input}`);
|
|
29
|
+
return normalizeDocsPath(url.pathname.slice(DOC_PREFIX.length));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function splitDocsPath(path: string): {
|
|
33
|
+
catalogName: "harmonyos-guides" | "harmonyos-references";
|
|
34
|
+
pagePath: string;
|
|
35
|
+
} {
|
|
36
|
+
const normalized = normalizeDocsPath(path);
|
|
37
|
+
if (normalized.startsWith("harmonyos-guides/")) {
|
|
38
|
+
return {
|
|
39
|
+
catalogName: "harmonyos-guides",
|
|
40
|
+
pagePath: normalized.slice("harmonyos-guides/".length),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (normalized.startsWith("harmonyos-references/")) {
|
|
44
|
+
return {
|
|
45
|
+
catalogName: "harmonyos-references",
|
|
46
|
+
pagePath: normalized.slice("harmonyos-references/".length),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Unsupported HarmonyOS documentation path: ${path}`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TOOL_DEFINITIONS } from "./mcp";
|
|
2
|
+
|
|
3
|
+
export function buildWebMcpManifest(origin = "https://hulistmi.ai") {
|
|
4
|
+
return {
|
|
5
|
+
name: "hulistmi.ai",
|
|
6
|
+
description: "AI-readable HarmonyOS documentation.",
|
|
7
|
+
mcp: `${origin}/mcp`,
|
|
8
|
+
tools: Object.entries(TOOL_DEFINITIONS).map(([name, definition]) => ({
|
|
9
|
+
name,
|
|
10
|
+
description: definition.description,
|
|
11
|
+
http: {
|
|
12
|
+
...definition.http,
|
|
13
|
+
path: definition.http.path
|
|
14
|
+
? `${origin}${definition.http.path}`
|
|
15
|
+
: undefined,
|
|
16
|
+
},
|
|
17
|
+
})),
|
|
18
|
+
};
|
|
19
|
+
}
|
package/wrangler.jsonc
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "hulistmi-ai",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"compatibility_date": "2026-03-10",
|
|
6
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
7
|
+
"assets": {
|
|
8
|
+
"binding": "ASSETS",
|
|
9
|
+
"directory": "./public",
|
|
10
|
+
"run_worker_first": ["/", "/.well-known/agent-skills/*"]
|
|
11
|
+
},
|
|
12
|
+
"observability": {
|
|
13
|
+
"enabled": true
|
|
14
|
+
},
|
|
15
|
+
"vars": {
|
|
16
|
+
"NODE_ENV": "production"
|
|
17
|
+
}
|
|
18
|
+
}
|