@mkterswingman/5mghost-wonder 0.0.1
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/auth/runtime.js +15 -0
- package/dist/cli.js +75 -0
- package/dist/commands/auth.js +100 -0
- package/dist/commands/check.js +258 -0
- package/dist/commands/help.js +38 -0
- package/dist/commands/index.js +50 -0
- package/dist/commands/read.js +198 -0
- package/dist/commands/setup.js +81 -0
- package/dist/commands/types.js +4 -0
- package/dist/commands/uninstall.js +14 -0
- package/dist/commands/update.js +21 -0
- package/dist/commands/version.js +8 -0
- package/dist/commands/wecom.js +136 -0
- package/dist/platform/npm.js +14 -0
- package/dist/platform/paths.js +25 -0
- package/dist/telemetry/events.js +42 -0
- package/dist/telemetry/policy.js +51 -0
- package/dist/telemetry/runtime.js +31 -0
- package/dist/wecom/browser.js +344 -0
- package/dist/wecom/cache.js +119 -0
- package/dist/wecom/cookies.js +151 -0
- package/dist/wecom/export.js +236 -0
- package/dist/wecom/url.js +45 -0
- package/dist/wecom/url.test.js +64 -0
- package/dist/xlsx/drawing.js +131 -0
- package/dist/xlsx/metadata.js +34 -0
- package/dist/xlsx/parse-tab.js +124 -0
- package/dist/xlsx/shared-strings.js +51 -0
- package/dist/xlsx/sheet.js +161 -0
- package/dist/xlsx/styles.js +85 -0
- package/dist/xlsx/unzip.js +33 -0
- package/dist/xlsx/workbook.js +51 -0
- package/dist/xlsx/workbook.test.js +19 -0
- package/package.json +41 -0
- package/scripts/check-export-types.mjs +37 -0
- package/scripts/postinstall.mjs +50 -0
- package/skills/setup-5mghost-wonder/SKILL.md +245 -0
- package/skills/use-5mghost-wonder/SKILL.md +240 -0
- package/skills.manifest.json +36 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
/**
|
|
3
|
+
* 解析 xl/sharedStrings.xml 内容,返回字符串池。
|
|
4
|
+
* 索引对应 sheet.xml 中 <c t="s"><v>N</v></c> 的 N。
|
|
5
|
+
*
|
|
6
|
+
* 支持两种 <si> 格式:
|
|
7
|
+
* - 简单文本:<si><t>text</t></si>
|
|
8
|
+
* - 富文本: <si><r><t>part1</t></r><r><t>part2</t></r></si>
|
|
9
|
+
*
|
|
10
|
+
* @param xml xl/sharedStrings.xml 的 UTF-8 文本内容
|
|
11
|
+
* @returns string[],下标即共享字符串索引
|
|
12
|
+
*/
|
|
13
|
+
export function parseSharedStrings(xml) {
|
|
14
|
+
const parser = new XMLParser({
|
|
15
|
+
ignoreAttributes: false,
|
|
16
|
+
// 关键:强制将 <si>、<r> 包装为数组,防止单元素时被解析为对象
|
|
17
|
+
isArray: (name) => name === "si" || name === "r",
|
|
18
|
+
// 保留空字符串(Excel 中存在空 <t/>)
|
|
19
|
+
parseTagValue: false,
|
|
20
|
+
// 保留空白字符(<t xml:space="preserve"> </t> 中的空格不应被 trim)
|
|
21
|
+
trimValues: false,
|
|
22
|
+
});
|
|
23
|
+
const doc = parser.parse(xml);
|
|
24
|
+
// xl/sharedStrings.xml 根节点是 <sst>
|
|
25
|
+
const sst = doc?.sst;
|
|
26
|
+
if (!sst)
|
|
27
|
+
return [];
|
|
28
|
+
const siList = sst.si ?? [];
|
|
29
|
+
return siList.map((si) => {
|
|
30
|
+
// 富文本:有 <r> 数组,拼接所有 <r><t> 内容
|
|
31
|
+
if (si.r) {
|
|
32
|
+
const runs = Array.isArray(si.r) ? si.r : [si.r];
|
|
33
|
+
return runs
|
|
34
|
+
.map((r) => textValue(r.t))
|
|
35
|
+
.join("");
|
|
36
|
+
}
|
|
37
|
+
// 简单文本:直接取 <t>
|
|
38
|
+
return textValue(si.t);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** 将 fast-xml-parser 解析出的 <t> 节点统一转为字符串。 */
|
|
42
|
+
function textValue(t) {
|
|
43
|
+
if (t == null)
|
|
44
|
+
return "";
|
|
45
|
+
// fast-xml-parser parseTagValue:false 时纯文本节点是字符串;
|
|
46
|
+
// 带属性的 <t xml:space="preserve"> 节点是对象 { "#text": "...", "@_xml:space": "..." }
|
|
47
|
+
if (typeof t === "object") {
|
|
48
|
+
return String(t["#text"] ?? "");
|
|
49
|
+
}
|
|
50
|
+
return String(t);
|
|
51
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// src/xlsx/sheet.ts
|
|
2
|
+
// Parses xl/worksheets/sheet{N}.xml.
|
|
3
|
+
// Outputs a sparse cell array, merge ranges, and bounding-box dimensions.
|
|
4
|
+
// Dependencies are injected: sharedStrings[] from shared-strings.ts,
|
|
5
|
+
// getFormatCode() from styles.ts. This module does not read files.
|
|
6
|
+
import { XMLParser } from "fast-xml-parser";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// XMLParser (module-level singleton — stateless, safe to reuse)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const parser = new XMLParser({
|
|
11
|
+
ignoreAttributes: false,
|
|
12
|
+
attributeNamePrefix: "@_",
|
|
13
|
+
// Force arrays to eliminate single-node vs. array ambiguity.
|
|
14
|
+
isArray: (name) => name === "row" || name === "c" || name === "mergeCell" || name === "r",
|
|
15
|
+
// Keep all values as strings; we convert manually based on the t attribute.
|
|
16
|
+
parseTagValue: false,
|
|
17
|
+
// Preserve whitespace (cells may contain spaces).
|
|
18
|
+
trimValues: false,
|
|
19
|
+
});
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Internal helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* Column letters (uppercase) → 0-based column index.
|
|
25
|
+
* A→0, Z→25, AA→26, AZ→51, BA→52.
|
|
26
|
+
*/
|
|
27
|
+
export function colLettersToIndex(letters) {
|
|
28
|
+
let result = 0;
|
|
29
|
+
for (let i = 0; i < letters.length; i++) {
|
|
30
|
+
result = result * 26 + (letters.charCodeAt(i) - 64); // 'A'=65 → 1
|
|
31
|
+
}
|
|
32
|
+
return result - 1; // convert to 0-based
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* "B5" → { row: 4, col: 1 } (0-based).
|
|
36
|
+
*/
|
|
37
|
+
export function parseCellRef(ref) {
|
|
38
|
+
const match = /^([A-Z]+)(\d+)$/.exec(ref);
|
|
39
|
+
if (!match)
|
|
40
|
+
throw new Error(`Invalid cell ref: "${ref}"`);
|
|
41
|
+
return {
|
|
42
|
+
col: colLettersToIndex(match[1]),
|
|
43
|
+
row: Number(match[2]) - 1,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* "A1:C3" → MergeRange (0-based).
|
|
48
|
+
*/
|
|
49
|
+
function parseMergeRef(ref) {
|
|
50
|
+
const sep = ref.indexOf(":");
|
|
51
|
+
if (sep === -1)
|
|
52
|
+
throw new Error(`Invalid merge ref: "${ref}"`);
|
|
53
|
+
const s = parseCellRef(ref.slice(0, sep));
|
|
54
|
+
const e = parseCellRef(ref.slice(sep + 1));
|
|
55
|
+
return { startRow: s.row, startCol: s.col, endRow: e.row, endCol: e.col };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract text from an <is> node (inlineStr).
|
|
59
|
+
* Handles simple <is><t>text</t></is> and rich-text <is><r><t>part</t></r>…</is>.
|
|
60
|
+
*/
|
|
61
|
+
function extractInlineStr(is) {
|
|
62
|
+
if (!is)
|
|
63
|
+
return "";
|
|
64
|
+
if (is.r) {
|
|
65
|
+
const runs = Array.isArray(is.r) ? is.r : [is.r];
|
|
66
|
+
return runs.map((r) => String(r?.t ?? "")).join("");
|
|
67
|
+
}
|
|
68
|
+
return String(is.t ?? "");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Extract a SheetCell from a single <c> node.
|
|
72
|
+
* Returns null when the cell should be skipped (error, no value, no ref).
|
|
73
|
+
*/
|
|
74
|
+
function extractCellValue(c, sharedStrings, getFormatCode) {
|
|
75
|
+
const ref = c["@_r"];
|
|
76
|
+
if (!ref)
|
|
77
|
+
return null;
|
|
78
|
+
const { row, col } = parseCellRef(ref);
|
|
79
|
+
const t = c["@_t"] ?? ""; // cell type; default = numeric
|
|
80
|
+
const sAttr = c["@_s"] != null ? Number(c["@_s"]) : -1;
|
|
81
|
+
// inlineStr: text lives in <is>, no <v>
|
|
82
|
+
if (t === "inlineStr") {
|
|
83
|
+
return { row, col, text: extractInlineStr(c.is) };
|
|
84
|
+
}
|
|
85
|
+
// Error cells: skip entirely
|
|
86
|
+
if (t === "e")
|
|
87
|
+
return null;
|
|
88
|
+
// All other types require <v>
|
|
89
|
+
const vRaw = c.v != null ? String(c.v) : undefined;
|
|
90
|
+
if (vRaw === undefined)
|
|
91
|
+
return null; // formula not calculated or empty cell
|
|
92
|
+
switch (t) {
|
|
93
|
+
case "s": {
|
|
94
|
+
// Shared string: <v> is the index into sharedStrings[]
|
|
95
|
+
const text = sharedStrings[Number(vRaw)] ?? "";
|
|
96
|
+
return { row, col, text };
|
|
97
|
+
}
|
|
98
|
+
case "str": {
|
|
99
|
+
// Formula result that is a string: <v> is the literal text
|
|
100
|
+
return { row, col, text: vRaw };
|
|
101
|
+
}
|
|
102
|
+
case "b": {
|
|
103
|
+
return { row, col, text: vRaw === "1" ? "TRUE" : "FALSE" };
|
|
104
|
+
}
|
|
105
|
+
default: {
|
|
106
|
+
// Numeric (t="n" or absent t)
|
|
107
|
+
const value = Number(vRaw);
|
|
108
|
+
if (isNaN(value))
|
|
109
|
+
return null;
|
|
110
|
+
const cell = { row, col, value };
|
|
111
|
+
if (sAttr >= 0) {
|
|
112
|
+
const format = getFormatCode(sAttr);
|
|
113
|
+
if (format !== undefined)
|
|
114
|
+
cell.format = format;
|
|
115
|
+
}
|
|
116
|
+
return cell;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Main export
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
export function parseSheet(xml, sharedStrings, getFormatCode) {
|
|
124
|
+
const doc = parser.parse(xml);
|
|
125
|
+
const ws = doc?.worksheet;
|
|
126
|
+
if (!ws)
|
|
127
|
+
return { cells: [], merges: [], maxRow: 0, maxCol: 0 };
|
|
128
|
+
// 1. Walk rows → cells
|
|
129
|
+
const cells = [];
|
|
130
|
+
let maxRow = 0;
|
|
131
|
+
let maxCol = 0;
|
|
132
|
+
const rows = ws.sheetData?.row ?? [];
|
|
133
|
+
for (const row of rows) {
|
|
134
|
+
const cList = row.c ?? [];
|
|
135
|
+
for (const c of cList) {
|
|
136
|
+
const cell = extractCellValue(c, sharedStrings, getFormatCode);
|
|
137
|
+
if (cell === null)
|
|
138
|
+
continue;
|
|
139
|
+
cells.push(cell);
|
|
140
|
+
if (cell.row > maxRow)
|
|
141
|
+
maxRow = cell.row;
|
|
142
|
+
if (cell.col > maxCol)
|
|
143
|
+
maxCol = cell.col;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// 2. Parse mergeCells
|
|
147
|
+
const merges = [];
|
|
148
|
+
const mergeCellList = ws.mergeCells?.mergeCell ?? [];
|
|
149
|
+
for (const mc of mergeCellList) {
|
|
150
|
+
const ref = mc["@_ref"];
|
|
151
|
+
if (!ref)
|
|
152
|
+
continue;
|
|
153
|
+
try {
|
|
154
|
+
merges.push(parseMergeRef(ref));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Malformed ref: skip without breaking overall parse
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { cells, merges, maxRow, maxCol };
|
|
161
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/xlsx/styles.ts
|
|
2
|
+
// Parses xl/styles.xml and provides a lookup from cell style index → format code.
|
|
3
|
+
//
|
|
4
|
+
// Three-layer mapping:
|
|
5
|
+
// cell.s → cellXfs[s].numFmtId → numFmtId → formatCode
|
|
6
|
+
//
|
|
7
|
+
// Built-in formats (numFmtId 0–49) are hardcoded per ECMA-376 §18.8.30.
|
|
8
|
+
// Custom formats (typically numFmtId ≥ 164) come from <numFmts> in styles.xml.
|
|
9
|
+
import { XMLParser } from "fast-xml-parser";
|
|
10
|
+
// ECMA-376 §18.8.30 built-in number format codes.
|
|
11
|
+
// numFmtId 0 (General) is intentionally included; callers must return undefined for it.
|
|
12
|
+
// IDs 5–8, 23–36, 41–44 are spec-reserved and omitted — lookups return undefined.
|
|
13
|
+
const BUILTIN_FORMATS = {
|
|
14
|
+
0: "General",
|
|
15
|
+
1: "0",
|
|
16
|
+
2: "0.00",
|
|
17
|
+
3: "#,##0",
|
|
18
|
+
4: "#,##0.00",
|
|
19
|
+
9: "0%",
|
|
20
|
+
10: "0.00%",
|
|
21
|
+
11: "0.00E+00",
|
|
22
|
+
12: "# ?/?",
|
|
23
|
+
13: "# ??/??",
|
|
24
|
+
14: "mm-dd-yy",
|
|
25
|
+
15: "d-mmm-yy",
|
|
26
|
+
16: "d-mmm",
|
|
27
|
+
17: "mmm-yy",
|
|
28
|
+
18: "h:mm AM/PM",
|
|
29
|
+
19: "h:mm:ss AM/PM",
|
|
30
|
+
20: "h:mm",
|
|
31
|
+
21: "h:mm:ss",
|
|
32
|
+
22: "m/d/yy h:mm",
|
|
33
|
+
37: "#,##0 ;(#,##0)",
|
|
34
|
+
38: "#,##0 ;[Red](#,##0)",
|
|
35
|
+
39: "#,##0.00;(#,##0.00)",
|
|
36
|
+
40: "#,##0.00;[Red](#,##0.00)",
|
|
37
|
+
45: "mm:ss",
|
|
38
|
+
46: "[h]:mm:ss",
|
|
39
|
+
47: "mmss.0",
|
|
40
|
+
48: "##0.0E+0",
|
|
41
|
+
49: "@",
|
|
42
|
+
};
|
|
43
|
+
function parseStyles(xml) {
|
|
44
|
+
const parser = new XMLParser({
|
|
45
|
+
ignoreAttributes: false,
|
|
46
|
+
attributeNamePrefix: "@_",
|
|
47
|
+
isArray: (tagName) => tagName === "xf" || tagName === "numFmt",
|
|
48
|
+
});
|
|
49
|
+
const parsed = parser.parse(xml);
|
|
50
|
+
const styleSheet = parsed?.styleSheet ?? parsed?.["styleSheet"] ?? parsed;
|
|
51
|
+
// 1. Initialise fmtMap with built-in formats
|
|
52
|
+
const fmtMap = new Map(Object.entries(BUILTIN_FORMATS).map(([k, v]) => [Number(k), v]));
|
|
53
|
+
// 2. Overlay custom formats from <numFmts>
|
|
54
|
+
const customFmts = styleSheet?.numFmts?.numFmt ?? [];
|
|
55
|
+
for (const item of customFmts) {
|
|
56
|
+
const id = Number(item["@_numFmtId"]);
|
|
57
|
+
const code = String(item["@_formatCode"] ?? "");
|
|
58
|
+
if (!Number.isNaN(id)) {
|
|
59
|
+
fmtMap.set(id, code);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 3. Build xfFmtIds from <cellXfs>
|
|
63
|
+
const xfs = styleSheet?.cellXfs?.xf ?? [];
|
|
64
|
+
const xfFmtIds = xfs.map((xf) => Number(xf["@_numFmtId"] ?? 0));
|
|
65
|
+
return { fmtMap, xfFmtIds };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build a lookup function from a styles.xml string.
|
|
69
|
+
*
|
|
70
|
+
* @param xml Content of xl/styles.xml, or "" if the file is absent.
|
|
71
|
+
* @returns A function: (styleIndex) => formatCode | undefined
|
|
72
|
+
* Returns undefined for numFmtId=0 (General) and out-of-bounds indices.
|
|
73
|
+
*/
|
|
74
|
+
export function buildStylesLookup(xml) {
|
|
75
|
+
if (!xml.trim()) {
|
|
76
|
+
return () => undefined;
|
|
77
|
+
}
|
|
78
|
+
const { fmtMap, xfFmtIds } = parseStyles(xml);
|
|
79
|
+
return (styleIndex) => {
|
|
80
|
+
const numFmtId = xfFmtIds[styleIndex];
|
|
81
|
+
if (numFmtId === undefined || numFmtId === 0)
|
|
82
|
+
return undefined;
|
|
83
|
+
return fmtMap.get(numFmtId); // returns undefined when not found
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/xlsx/unzip.ts
|
|
2
|
+
// Phase 2: Thin wrapper around JSZip for reading xlsx entries.
|
|
3
|
+
import JSZip from "jszip";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
/**
|
|
6
|
+
* Open an xlsx file and return an XlsxZip handle.
|
|
7
|
+
* The entire ZIP is loaded into memory once; subsequent reads are synchronous
|
|
8
|
+
* decompression so they are fast for typical xlsx sizes.
|
|
9
|
+
*/
|
|
10
|
+
export async function openXlsx(filePath) {
|
|
11
|
+
const buffer = await readFile(filePath);
|
|
12
|
+
const zip = await JSZip.loadAsync(buffer);
|
|
13
|
+
return {
|
|
14
|
+
async readEntry(path) {
|
|
15
|
+
const file = zip.file(path);
|
|
16
|
+
if (!file)
|
|
17
|
+
return null;
|
|
18
|
+
return file.async("string");
|
|
19
|
+
},
|
|
20
|
+
async readEntryBuffer(path) {
|
|
21
|
+
const file = zip.file(path);
|
|
22
|
+
if (!file)
|
|
23
|
+
return null;
|
|
24
|
+
return file.async("nodebuffer");
|
|
25
|
+
},
|
|
26
|
+
entryNames() {
|
|
27
|
+
return Object.keys(zip.files);
|
|
28
|
+
},
|
|
29
|
+
get rawJSZip() {
|
|
30
|
+
return zip;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/xlsx/workbook.ts
|
|
2
|
+
// Phase 2-01: Parse xl/workbook.xml + xl/_rels/workbook.xml.rels
|
|
3
|
+
import { XMLParser } from "fast-xml-parser";
|
|
4
|
+
/**
|
|
5
|
+
* Parse xl/workbook.xml + xl/_rels/workbook.xml.rels from an open XlsxZip.
|
|
6
|
+
* Includes hidden tabs (hidden: true).
|
|
7
|
+
* Returns tabs in workbook.xml document order.
|
|
8
|
+
*/
|
|
9
|
+
export async function parseWorkbook(zip) {
|
|
10
|
+
const parser = new XMLParser({
|
|
11
|
+
ignoreAttributes: false,
|
|
12
|
+
attributeNamePrefix: "@_",
|
|
13
|
+
parseAttributeValue: false,
|
|
14
|
+
isArray: (name) => name === "sheet" || name === "Relationship",
|
|
15
|
+
});
|
|
16
|
+
// --- workbook.xml ---
|
|
17
|
+
const wbXml = await zip.readEntry("xl/workbook.xml");
|
|
18
|
+
if (!wbXml)
|
|
19
|
+
throw new Error("xl/workbook.xml not found in xlsx archive");
|
|
20
|
+
const wbParsed = parser.parse(wbXml);
|
|
21
|
+
// Navigate: workbook.sheets.sheet[]
|
|
22
|
+
const wb = wbParsed["workbook"];
|
|
23
|
+
const sheets = wb["sheets"]["sheet"];
|
|
24
|
+
// --- workbook.xml.rels ---
|
|
25
|
+
const relsXml = await zip.readEntry("xl/_rels/workbook.xml.rels");
|
|
26
|
+
if (!relsXml)
|
|
27
|
+
throw new Error("xl/_rels/workbook.xml.rels not found in xlsx archive");
|
|
28
|
+
const relsParsed = parser.parse(relsXml);
|
|
29
|
+
const relationships = relsParsed["Relationships"]["Relationship"];
|
|
30
|
+
// Build rId → Target map, only worksheet type entries
|
|
31
|
+
const rIdToPath = new Map();
|
|
32
|
+
for (const rel of relationships) {
|
|
33
|
+
const type = rel["@_Type"] ?? "";
|
|
34
|
+
if (type.endsWith("/worksheet")) {
|
|
35
|
+
rIdToPath.set(rel["@_Id"], rel["@_Target"]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Merge
|
|
39
|
+
const tabs = sheets.map((sheet) => {
|
|
40
|
+
// r:id attribute is parsed as "@_r:id" by fast-xml-parser
|
|
41
|
+
const rId = sheet["@_r:id"] ?? "";
|
|
42
|
+
return {
|
|
43
|
+
name: sheet["@_name"] ?? "",
|
|
44
|
+
sheetId: sheet["@_sheetId"] ?? "",
|
|
45
|
+
rId,
|
|
46
|
+
hidden: sheet["@_state"] === "hidden",
|
|
47
|
+
sheetPath: rIdToPath.get(rId) ?? "",
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
return { tabs };
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/xlsx/workbook.test.ts — Node assert, no framework
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { openXlsx } from "./unzip.js";
|
|
4
|
+
import { parseWorkbook } from "./workbook.js";
|
|
5
|
+
const REAL_FILE = "/Users/yimingwang/Downloads/盛世天下-Steam素材交付.xlsx";
|
|
6
|
+
const zip = await openXlsx(REAL_FILE);
|
|
7
|
+
const { tabs } = await parseWorkbook(zip);
|
|
8
|
+
assert.equal(tabs.length, 10, `Expected 10 tabs, got ${tabs.length}`);
|
|
9
|
+
assert.equal(tabs[0].name, "上部Steam商店页预览");
|
|
10
|
+
assert.equal(tabs[3].hidden, true, "tabs[3] should be hidden");
|
|
11
|
+
assert.equal(tabs[3].sheetPath, "worksheets/sheet4.xml");
|
|
12
|
+
assert.equal(tabs[5].hidden, true, "tabs[5] should be hidden");
|
|
13
|
+
assert.equal(tabs[9].name, "下部Steam商店页预览");
|
|
14
|
+
assert.equal(tabs[9].sheetPath, "worksheets/sheet10.xml");
|
|
15
|
+
for (const tab of tabs) {
|
|
16
|
+
assert.ok(tab.sheetPath.length > 0, `Tab "${tab.name}" has empty sheetPath`);
|
|
17
|
+
}
|
|
18
|
+
console.log("✅ parseWorkbook: all assertions passed");
|
|
19
|
+
console.log(tabs.map((t) => `${t.hidden ? "[hidden] " : ""}${t.name} -> ${t.sheetPath}`).join("\n"));
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mkterswingman/5mghost-wonder",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "企微文档读取 CLI — WeCom document reader",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": { "node": ">=18" },
|
|
7
|
+
"bin": { "wonder": "./dist/cli.js" },
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/5mghost/5mghost-wonder.git"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"skills",
|
|
18
|
+
"skills.manifest.json",
|
|
19
|
+
"scripts"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "node dist/wecom/url.test.js",
|
|
25
|
+
"smoke": "npm run build && node dist/cli.js help > /dev/null",
|
|
26
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@mkterswingman/5mghost-auth": "^0.0.1",
|
|
30
|
+
"@mkterswingman/5mghost-telemetry": "^0.0.1",
|
|
31
|
+
"@mkterswingman/5mghost-agent-skills": "^0.0.1",
|
|
32
|
+
"ws": "^8.18.0",
|
|
33
|
+
"fast-xml-parser": "^4.5.0",
|
|
34
|
+
"jszip": "^3.10.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"@types/ws": "^8.5.0",
|
|
39
|
+
"typescript": "^5.8.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// scripts/check-export-types.mjs
|
|
2
|
+
// Smoke test: verify that the compiled export module exports the expected symbols.
|
|
3
|
+
// Run after build: node scripts/check-export-types.mjs
|
|
4
|
+
|
|
5
|
+
import { parseWecomUrl } from "../dist/wecom/url.js";
|
|
6
|
+
import { exportWecomDoc, ExportError } from "../dist/wecom/export.js";
|
|
7
|
+
|
|
8
|
+
// Verify exported symbols are functions/classes
|
|
9
|
+
if (typeof parseWecomUrl !== "function") throw new Error("parseWecomUrl not a function");
|
|
10
|
+
if (typeof exportWecomDoc !== "function") throw new Error("exportWecomDoc not a function");
|
|
11
|
+
if (typeof ExportError !== "function") throw new Error("ExportError not a class");
|
|
12
|
+
|
|
13
|
+
// Verify ExportError is throwable
|
|
14
|
+
const err = new ExportError("poll_timeout", "test error");
|
|
15
|
+
if (err.name !== "ExportError") throw new Error("ExportError.name wrong");
|
|
16
|
+
if (err.kind !== "poll_timeout") throw new Error("ExportError.kind wrong");
|
|
17
|
+
if (err.message !== "test error") throw new Error("ExportError.message wrong");
|
|
18
|
+
if (!(err instanceof Error)) throw new Error("ExportError not instanceof Error");
|
|
19
|
+
|
|
20
|
+
// Verify parseWecomUrl returns correct discriminated union shape
|
|
21
|
+
const parsed = parseWecomUrl("https://doc.weixin.qq.com/sheet/e3_TestId");
|
|
22
|
+
if (!parsed.ok) throw new Error("parseWecomUrl failed for valid sheet URL");
|
|
23
|
+
if (parsed.docType !== "sheet") throw new Error(`docType wrong: ${parsed.docType}`);
|
|
24
|
+
if (parsed.docId !== "e3_TestId") throw new Error(`docId wrong: ${parsed.docId}`);
|
|
25
|
+
|
|
26
|
+
const smartpage = parseWecomUrl("https://doc.weixin.qq.com/smartpage/a1_Test");
|
|
27
|
+
if (smartpage.ok) throw new Error("smartpage should be unsupported");
|
|
28
|
+
if (smartpage.reason !== "unsupported" || smartpage.kind !== "smartpage") {
|
|
29
|
+
throw new Error(`smartpage narrowing wrong: ${JSON.stringify(smartpage)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const bad = parseWecomUrl("not-a-url");
|
|
33
|
+
if (bad.ok || bad.reason !== "invalid") {
|
|
34
|
+
throw new Error(`bad URL should reason=invalid: ${JSON.stringify(bad)}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log("✅ export module: all symbols present and structurally correct");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// scripts/postinstall.mjs
|
|
2
|
+
// Runs after `npm install -g` to install wonder skills into detected AI clients.
|
|
3
|
+
// Must be plain Node ESM — no TypeScript, no Bun dependencies.
|
|
4
|
+
// This file runs from the installed package directory, so paths are resolved
|
|
5
|
+
// relative to import.meta.url.
|
|
6
|
+
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { dirname, resolve } from "path";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
let listDetectedAgents, installSkills;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
({ listDetectedAgents, installSkills } = await import(
|
|
16
|
+
"@mkterswingman/5mghost-agent-skills"
|
|
17
|
+
));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
// agent-skills not yet available (e.g. first install before deps resolve)
|
|
20
|
+
console.log("[wonder] agent-skills not available — skipping skill install.");
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const manifestPath = resolve(__dirname, "../skills.manifest.json");
|
|
25
|
+
|
|
26
|
+
let agents;
|
|
27
|
+
try {
|
|
28
|
+
agents = await listDetectedAgents();
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.log("[wonder] Could not detect AI clients — skipping skill install.");
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!agents || agents.length === 0) {
|
|
35
|
+
console.log("[wonder] No AI clients detected — skipping skill install.");
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result = await installSkills({ manifestPath, detectedAgents: agents });
|
|
41
|
+
const installed = result.installedTo ?? [];
|
|
42
|
+
if (installed.length > 0) {
|
|
43
|
+
console.log(`[wonder] Installed skills to: ${installed.join(", ")}`);
|
|
44
|
+
} else {
|
|
45
|
+
console.log("[wonder] No skill targets updated.");
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// Skill install failure must never break npm install
|
|
49
|
+
console.log(`[wonder] Skill install failed (non-fatal): ${String(err)}`);
|
|
50
|
+
}
|