@wwkit/harness 1.0.4

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -0
  3. package/agents/extract.md +38 -0
  4. package/agents/query.md +54 -0
  5. package/agents/revise.md +40 -0
  6. package/package.json +55 -0
  7. package/plugin.js +19 -0
  8. package/readme/development.md +37 -0
  9. package/readme/publish.md +38 -0
  10. package/readme/testing.md +36 -0
  11. package/skills/extract/SKILL.md +100 -0
  12. package/skills/extract/references/build-xpath.js +31 -0
  13. package/skills/extract/references/clean-html.js +91 -0
  14. package/skills/extract/references/detail.md +60 -0
  15. package/skills/extract/references/detail.schema.json5 +49 -0
  16. package/skills/extract/references/extract-detail.js +72 -0
  17. package/skills/extract/references/extract-regex.js +82 -0
  18. package/skills/extract/references/extract-sample.js +49 -0
  19. package/skills/extract/references/format-aliases.json5 +22 -0
  20. package/skills/extract/references/input.schema.json5 +23 -0
  21. package/skills/extract/references/list-rule-gen.md +109 -0
  22. package/skills/extract/references/list.schema.json5 +34 -0
  23. package/skills/extract/references/list_from_html.md +74 -0
  24. package/skills/extract/references/list_from_json.md +52 -0
  25. package/skills/extract/references/list_from_text.md +60 -0
  26. package/skills/extract/references/navi.md +49 -0
  27. package/skills/extract/references/navi.schema.json5 +23 -0
  28. package/skills/extract/references/text.md +46 -0
  29. package/skills/extract/references/text.schema.json5 +22 -0
  30. package/skills/extract/references/to-text.js +21 -0
  31. package/skills/extract/references/validate-schema.js +40 -0
  32. package/skills/revise/SKILL.md +113 -0
  33. package/skills/revise/references/article.md +44 -0
  34. package/skills/revise/references/article.schema.json5 +40 -0
  35. package/skills/revise/references/format-aliases.json5 +22 -0
  36. package/skills/revise/references/gallery.md +43 -0
  37. package/skills/revise/references/gallery.schema.json5 +40 -0
  38. package/skills/revise/references/input.schema.json5 +28 -0
  39. package/skills/revise/references/question.md +41 -0
  40. package/skills/revise/references/question.schema.json5 +31 -0
  41. package/skills/revise/references/status.md +40 -0
  42. package/skills/revise/references/status.schema.json5 +27 -0
  43. package/skills/revise/references/validate-schema.js +40 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 macair15m3
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # harness
2
+
3
+ > `wwkit` monorepo 下的子包(`packages/harness`),原为 `webwork-ability`。
4
+
5
+ WebWork abilities for opencode — 提供内容提取(extract)、内容创作(revise)等技能与代理,让 opencode 具备从网页提取结构化内容、基于素材二次创作等能力。
6
+
7
+ ## 前提
8
+
9
+ 技能依赖 `node` 运行辅助脚本(如 `references/validate-schema.js`)。调用 opencode 的环境需确保 `node` 在 PATH 中(opencode 的 bash 工具会继承其启动时的环境)。若用 Python 等子进程方式调用,请在 spawn 前把 node 所在目录加入 PATH:
10
+
11
+ ```python
12
+ import shutil, os
13
+ node = shutil.which("node") or os.path.expanduser("~/.nvm/versions/node/*/bin/node")
14
+ os.environ["PATH"] = f"{os.path.dirname(node)}:{os.environ.get('PATH', '')}"
15
+ ```
16
+
17
+ ## 安装
18
+
19
+ 本包位于 `wwkit` pnpm monorepo 的 `packages/harness`。在仓库根目录安装依赖:
20
+
21
+ ```bash
22
+ cd wwkit
23
+ pnpm install
24
+ ```
25
+
26
+ ### 在 opencode 中使用(发布为 npm 包后)
27
+
28
+ ```bash
29
+ cd ~/.config/opencode
30
+ npm install harness
31
+ ```
32
+
33
+ ## 引入方式
34
+
35
+ 本包提供两种引入方式,按需选择一种:
36
+
37
+ | 方式 | 配置 | 提供内容 | 适用 |
38
+ |------|------|---------|------|
39
+ | **A. 插件模式(仅技能)** | 在 `~/.config/opencode/opencode.jsonc` 注册插件 | 仅 `extract`/`revise` **技能** | 只需要技能能力 |
40
+ | **B. 环境变量模式(完整,推荐)** | 设置 `OPENCODE_CONFIG_DIR` 指向包目录 | **代理 + 技能**(原生扫描 `agents/`、`skills/`) | 需要代理完整能力 |
41
+
42
+ ### 方式 A:插件模式(仅技能)
43
+
44
+ 在 `~/.config/opencode/opencode.jsonc` 中注册插件:
45
+
46
+ ```jsonc
47
+ {
48
+ "plugin": ["node_modules/harness"]
49
+ }
50
+ ```
51
+
52
+ 该模式仅注册技能(`extract`/`revise`)。opencode 不支持通过插件注册代理(`agents.paths` 字段不存在),因此**方式 A 不含代理**。
53
+
54
+ ### 方式 B:环境变量模式(完整,推荐)
55
+
56
+ 将 `OPENCODE_CONFIG_DIR` 指向包的安装目录,opencode 会把它当作配置目录,原生发现其中的 `agents/`、`skills/`:
57
+
58
+ ```bash
59
+ echo 'export OPENCODE_CONFIG_DIR="$HOME/.config/opencode/node_modules/harness"' >> ~/.zshrc
60
+ ```
61
+
62
+ 说明:
63
+ - 需在**启动 opencode 前**设置环境变量;
64
+ - 与用户原有配置目录是**叠加**关系,不覆盖已有代理/技能;
65
+ - 该变量为**单一路径**,同时只能指向一个目录;
66
+ - 若之前注册过插件(方式 A),改用方式 B 时请移除 `opencode.jsonc` 中的 `plugin` 项,避免技能重复扫描。
67
+
68
+ ## 功能
69
+
70
+ ### Agents(代理)
71
+
72
+ | Agent | 作用 |
73
+ |-------|------|
74
+ | `extract` | 从网页 HTML/文本/JSON 提取结构化内容(列表/详情/纯文本/导航),写入输出文件 |
75
+ | `revise` | 基于原始素材二次创作(问题/文章/图集/动态),写入输出文件 |
76
+ | `query` | 根据 prompt 生成内容,支持 source/language/output |
77
+
78
+ ### Skills(技能)
79
+
80
+ | Skill | 作用 |
81
+ |-------|------|
82
+ | `extract` | 按 format(list/detail/text/navi)从 HTML/文本/JSON 提取内容 |
83
+ | `revise` | 按 format(question/status/gallery/article)二次创作内容 |
84
+
85
+ ## 使用方式
86
+
87
+ ```bash
88
+ # 使用 extract agent:从 HTML 或文本提取列表
89
+ opencode run --agent extract '{"format": "list", "source": "<html/文本或路径>", "output": "/tmp/list.json"}'
90
+
91
+ # 使用 extract agent:提取单条详情
92
+ opencode run --agent extract '{"format": "detail", "source": "<html或路径>", "output": "/tmp/result.json"}'
93
+
94
+ # 使用 revise agent:基于素材创作文章
95
+ opencode run --agent revise '{"format": "article", "source": "<素材>", "output": "/tmp/article.json"}'
96
+
97
+ # 使用 query agent:直接生成内容
98
+ opencode run --agent query '{"prompt": "写一篇关于AI的文章"}'
99
+ ```
100
+
101
+ 未提供 `output` 时,结果直接输出到 stdout。
@@ -0,0 +1,38 @@
1
+ ---
2
+ description: 从网页 HTML/文本源码提取结构化内容(list/detail/text/navi),
3
+ 通过 extract 技能生成 JSON 数组并写入输出文件。
4
+ mode: primary
5
+ temperature: 0.2
6
+ permission:
7
+ skill: allow
8
+ read: allow
9
+ edit: allow
10
+ webfetch: deny
11
+ bash:
12
+ "*": deny
13
+ "node*": allow
14
+ "ls*": allow
15
+ "cat*": allow
16
+ "test*": allow
17
+ "echo*": allow
18
+ "pwd*": allow
19
+ ---
20
+
21
+ 你是内容提取编辑。收到任务消息后,提取其中的参数执行:
22
+
23
+ 1. 用 `skill` 工具加载 `extract` 技能,并用 `read` 读取技能目录下的 `references/input.schema.json5`(入参字段清单)。
24
+ 2. 解析入参。任务消息可能含以下任一形态:
25
+ - **JSON 对象**:如 `{format, source, url}`,直接取字段值;
26
+ - **key=value**:如 `format=list, source=xxx`,拆分为字段;
27
+ - **纯文本 prose**:不做格式推断。此时用 `read` 读取技能目录下的 `references/format-aliases.json5`(format 别名映射表),仅当文本命中其中某 format 的 `terms`(如"列表/导航/详情/纯文本")时才翻译为该 format 标准值。
28
+ 之后的 `format`(必填,经别名表翻译;list 由技能自动检测 source 是 HTML 还是 JSON)、`source`(必填,HTML/文本或文件路径,原样透传由技能解析)、`url`(可选,相对路径转绝对)、`output`(可选)。其他参数(如 count/max_length/max_image_count/language)被技能忽略。若无法解析出必填的 `format` 或 `source`,将错误信息输出到 stderr 并结束,**禁止**调用技能。
29
+ 3. 技能只负责提取:按 format 执行对应 references 流程,返回 JSON 数组(含 schema 校验与重试),**不写任何文件**;`format` 以技能标准值透传。
30
+ 4. `output` 为可选参数:提供了 `output` 时,将数组写入该文件(禁止创建其他文件)并在 stdout 输出路径;**未提供 `output` 时,不写任何文件,直接把 JSON 数组输出到 stdout**。
31
+ 5. 若技能校验失败未能产出数组,则不写文件/不输出数组,将错误信息输出到 stderr 并结束。
32
+ 6. 禁止访问外部网络;除技能校验/本地提取所需 node 外,禁止执行其他 shell。
33
+
34
+ ## 工具使用约束
35
+
36
+ - 写文件一律用 `write` 工具;读文件用 `read` 工具。
37
+ - 需要中间数据时,用 `write` 工具写入临时文件,再以 stdin 重定向传给 node。
38
+ - 禁止使用未授权的 `cp`/`rm`/`mv` 等命令;需要复制、移动或删除临时文件时,一律用允许的 `node -e` 的 fs 模块完成(如 `require('fs').copyFileSync(...)`、`require('fs').unlinkSync(...)`)。
@@ -0,0 +1,54 @@
1
+ ---
2
+ description: 根据用户要求生成内容,支持 prompt/source/language/output 参数并按 revise 风格输出。
3
+ mode: primary
4
+ temperature: 0.7
5
+ permission:
6
+ skill: allow
7
+ read: allow
8
+ edit: allow
9
+ webfetch: deny
10
+ bash:
11
+ "*": deny
12
+ "node*": allow
13
+ "ls*": allow
14
+ "cat*": allow
15
+ "test*": allow
16
+ "echo*": allow
17
+ "pwd*": allow
18
+ ---
19
+
20
+ 你是内容生成编辑。收到任务消息后,提取其中的参数执行:
21
+
22
+ ## 入参字段
23
+
24
+ | 参数 | 类型 | 必填 | 默认 | 说明 |
25
+ |------|------|------|------|------|
26
+ | `prompt` | string | 是 | - | 任务要求,包含输出格式说明 |
27
+ | `source` | string | 否 | - | 原始素材:普通文本或文件路径(可选) |
28
+ | `language` | string | 否 | 中文 | 输出语言 |
29
+ | `output` | string | 否 | - | 输出文件路径(可选,未提供时输出到 stdout) |
30
+
31
+ ## 解析入参
32
+
33
+ 任务消息可能为以下任一形态:
34
+
35
+ - **JSON 对象**:如 `{"prompt": "写一篇关于AI的文章", "output": "/tmp/out.txt"}`,直接取字段值;
36
+ - **key=value**:如 `prompt=写一篇关于AI的文章, output=/tmp/out.txt`,按 `=` 拆分键值对,字段名匹配上表;
37
+ - **自然语言**:尝试从文本中推断提取参数(如"输出到xxx"→`output`、"素材xxx"→`source`、"用英文"→`language`),剩余内容作为 `prompt`;若无法提取,整段文本作为 `prompt`。
38
+
39
+ ## 执行
40
+
41
+ 1. 若 `prompt` 为空或仅含空白字符,将错误信息输出到 stderr 并结束,**禁止**继续执行。
42
+ 2. 利用自身能力按 `prompt` 要求生成内容:
43
+ - `source` 非空时,若值为现有文件路径则读取文件内容,否则直接以值作为素材
44
+ - 输出语言使用 `language`(默认中文)
45
+ 3. 输出处理:
46
+ - **提供了 `output`**:将结果写入该文件(禁止创建其他文件),在 stdout 输出路径
47
+ - **未提供 `output`**:不写文件,直接把结果输出到 stdout
48
+ 4. 若结果为空或生成失败,将错误信息输出到 stderr 并结束。
49
+
50
+ ## 工具使用约束
51
+
52
+ - 写文件一律用 `write` 工具;读文件用 `read` 工具。
53
+ - 需要中间数据时,用 `write` 工具写入临时文件,再以 stdin 重定向传给 node。
54
+ - 禁止使用未授权的 `cp`/`rm`/`mv` 等命令;需要复制、移动或删除临时文件时,一律用允许的 `node -e` 的 fs 模块完成(如 `require('fs').copyFileSync(...)`、`require('fs').unlinkSync(...)`)。
@@ -0,0 +1,40 @@
1
+ ---
2
+ description: 基于原始素材进行二次创作(question/status/gallery/article),
3
+ 通过 revise 技能生成 JSON 数组并写入输出文件。
4
+ mode: primary
5
+ temperature: 0.7
6
+ permission:
7
+ skill: allow
8
+ read: allow
9
+ edit: allow
10
+ webfetch: deny
11
+ bash:
12
+ "*": deny
13
+ "node*": allow
14
+ "ls*": allow
15
+ "cat*": allow
16
+ "test*": allow
17
+ "echo*": allow
18
+ "pwd*": allow
19
+ ---
20
+
21
+ 你是内容创作编辑。收到任务消息后,提取其中的参数执行:
22
+
23
+ 1. 用 `skill` 工具加载 `revise` 技能,并用 `read` 读取技能目录下的 `references/input.schema.json5`(入参字段清单)。
24
+ 2. 解析入参。任务消息可能为以下任一形态:
25
+ - **JSON 对象**:如 `{format, source, count, language}`,直接取字段值;
26
+ - **key=value**:如 `format=question, source=xxx`,拆分为字段;
27
+ - **纯文本 prose**:不做格式推断(同一 source 可有多种 format)。此时用 `read` 读取技能目录下的 `references/format-aliases.json5`(format 别名映射表),仅当文本命中其中某 format 的 `terms`(如"问题/文章/图集/动态")时才翻译为该 format 标准值。
28
+ 之后的字段:
29
+ - `format`(必填,经别名表翻译为标准值)、`source`(必填,素材文本或文件路径,原样透传由技能解析)、`count`(可选,数值,缺省透传空由技能按默认 1 处理)、`language`(可选,缺省透传空由技能按默认 中文 处理)、`output`(输出文件路径,可选)。
30
+ 若无法解析出必填的 `format` 或 `source`,将错误信息输出到 stderr 并结束,**禁止**调用技能。
31
+ 3. 技能只负责执行内容:按 source/format/count/language 生成 JSON 数组(含 schema 校验与重试),**不写任何文件**;`format` 以本步骤翻译后的标准值透传。
32
+ 4. `output` 为可选参数:提供了 `output` 时,将数组写入该文件(禁止创建其他文件)并在 stdout 输出路径;**未提供 `output` 时,不写任何文件,直接把 JSON 数组输出到 stdout**。
33
+ 5. 若技能校验失败未能产出数组,则不写文件/不输出数组,将错误信息输出到 stderr 并结束。
34
+ 6. 禁止访问外部网络;除技能校验所需 node 外,禁止执行其他 shell。
35
+
36
+ ## 工具使用约束
37
+
38
+ - 写文件一律用 `write` 工具;读文件用 `read` 工具。
39
+ - 需要中间数据时,用 `write` 工具写入临时文件,再以 stdin 重定向传给 node。
40
+ - 禁止使用未授权的 `cp`/`rm`/`mv` 等命令;需要复制、移动或删除临时文件时,一律用允许的 `node -e` 的 fs 模块完成(如 `require('fs').copyFileSync(...)`、`require('fs').unlinkSync(...)`)。
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@wwkit/harness",
3
+ "version": "1.0.4",
4
+ "author": "bluesliu <langcai163@163.com>",
5
+ "description": "WebWork abilities for opencode",
6
+ "type": "module",
7
+ "main": "plugin.js",
8
+ "files": [
9
+ "plugin.js",
10
+ "skills",
11
+ "agents",
12
+ "readme"
13
+ ],
14
+ "jest": {
15
+ "transform": {},
16
+ "moduleNameMapper": {},
17
+ "testMatch": [
18
+ "**/tests/unit/**/*.test.js",
19
+ "**/tests/e2e/**/*.test.cjs"
20
+ ],
21
+ "setupFiles": [
22
+ "./jest.setup.cjs"
23
+ ]
24
+ },
25
+ "keywords": [
26
+ "opencode",
27
+ "plugin",
28
+ "skills"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git@gitee.com:Nameless_2/webwork-ability.git"
33
+ },
34
+ "license": "MIT",
35
+ "publishConfig": {
36
+ "registry": "https://registry.npmjs.org/",
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "ajv": "^8.17.0",
41
+ "cheerio": "^1.0.0",
42
+ "json5": "^2.2.3"
43
+ },
44
+ "devDependencies": {
45
+ "@opencode-ai/plugin": "1.18.18",
46
+ "jest": "^29.7.0"
47
+ },
48
+ "scripts": {
49
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --no-cache",
50
+ "test:all": "node --experimental-vm-modules node_modules/jest/bin/jest.js --no-cache tests/unit",
51
+ "e2e": "node --experimental-vm-modules node_modules/jest/bin/jest.js --no-cache",
52
+ "e2e:all": "node --experimental-vm-modules node_modules/jest/bin/jest.js --no-cache tests/e2e",
53
+ "release": "pnpm version patch && pnpm publish"
54
+ }
55
+ }
package/plugin.js ADDED
@@ -0,0 +1,19 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+
6
+ export const WebWorkAbilityPlugin = async () => {
7
+ const root = path.resolve(__dirname);
8
+
9
+ return {
10
+ config: async (config) => {
11
+ const skillsDir = path.join(root, 'skills');
12
+ config.skills = config.skills || {};
13
+ config.skills.paths = config.skills.paths || [];
14
+ if (!config.skills.paths.includes(skillsDir)) {
15
+ config.skills.paths.push(skillsDir);
16
+ }
17
+ }
18
+ };
19
+ };
@@ -0,0 +1,37 @@
1
+ # 开发
2
+
3
+ ## Monorepo 结构
4
+
5
+ 本包位于 `wwkit` pnpm monorepo 下:
6
+
7
+ ```
8
+ wwkit/
9
+ ├── package.json # 仓库根,聚合脚本(pnpm -r --filter ./packages/**)
10
+ ├── pnpm-workspace.yaml # 工作区声明(packages/*)+ allowBuilds
11
+ ├── pnpm-lock.yaml
12
+ └── packages/
13
+ └── harness/ # 本包(原 webwork-ability,opencode 插件)
14
+ ```
15
+
16
+ ## 仓库结构(packages/harness 内)
17
+
18
+ ```
19
+ plugin.js # 包入口("main"),仅把 skills/ 注册进 config.skills.paths
20
+ agents/*.md # 主代理(extract / revise / query),frontmatter 含权限块
21
+ skills/<name>/SKILL.md # 技能定义
22
+ skills/<name>/references/ # 工作流:*.md(LLM 指令)、*.js(ESM 辅助脚本)、*.json5(schema)
23
+ tests/unit/ # 单元测试(ESM)
24
+ tests/e2e/ # E2E 测试(CommonJS,真实调用 opencode run)
25
+ readme/ # 分模块文档(发布 / 测试 / 开发)
26
+ ```
27
+
28
+ ## 约定
29
+
30
+ - 开发与改动约定的权威说明见 `AGENTS.md`(文件格式 ESM/CJS 边界、技能工作流、schema 校验、无网络等约束)。
31
+ - 两种引入方式:**插件模式**仅注册技能(`plugin.js` 经 `config.skills.paths`);**环境变量模式**(`OPENCODE_CONFIG_DIR` 指向包目录)原生发现代理与技能。两者二选一,避免技能重复扫描。
32
+ - 新增技能只需在 `skills/` 添加目录,新增代理只需在 `agents/` 添加 `.md` 文件,均无需改 `plugin.js`。
33
+ - 新增 `revise`/`extract` 的 format 需同时提供 `<format>.md` 与 `<format>.schema.json5`(schema 为数组,`minItems: 1`)。
34
+
35
+ ## 测试
36
+
37
+ 测试命令见 [`readme/testing.md`](./testing.md),发布步骤见 [`readme/publish.md`](./publish.md)。
@@ -0,0 +1,38 @@
1
+ # 发布 npm 包
2
+
3
+ 发布配置已就绪:`package.json` 的 `files` 白名单(仅发布 `plugin.js`、`skills/`、`agents/`)、`prepublishOnly`(发布前自动跑单元测试)、`publishConfig.registry`(发布强制官方源,安装仍走国内镜像)。
4
+
5
+ 发布需在包目录 `packages/harness` 下执行 pnpm 命令:
6
+
7
+ ```bash
8
+ # 0. 一键发布(升 patch 版本 + 发布,等价于 pnpm version patch && pnpm publish)
9
+ pnpm run release
10
+
11
+ # 1. 检查将发布的内容(应为 40 个文件左右,不含 tests/ 等)
12
+ pnpm pack --dry-run
13
+
14
+ # 2. 升级版本(patch/minor/major 按需)
15
+ pnpm version patch
16
+
17
+ # 3. 认证:将 npmjs 的 Access Token 写入用户级 ~/.npmrc
18
+ # //registry.npmjs.org/:_authToken=npm_xxxxx
19
+ # 若账号开启了 2FA,token 需在创建时勾选 "Bypass 2FA",否则发布时需加 --otp=<动态码>
20
+
21
+ # 4. 发布(自动先跑 pnpm run test:all)
22
+ pnpm publish
23
+
24
+ # 5. 验证
25
+ # 默认走仓库 .npmrc 的国内镜像(同步快,一般即可确认)
26
+ npm view harness version dist-tags.latest
27
+ # 显式指定官方源确认发布成功
28
+ npm view harness version dist-tags.latest --registry=https://registry.npmjs.org/
29
+ # 查看已发布的所有版本
30
+ npm view harness versions
31
+ ```
32
+
33
+ ## 注意事项
34
+
35
+ - token 只写入 `~/.npmrc`(用户级),不要写进仓库的 `.npmrc` 或提交到 git。
36
+ - 仓库 `.npmrc` 指向国内镜像(淘宝 npmmirror),只影响安装;发布由 `publishConfig.registry` 强制走官方源。
37
+ - 包名在官方源首次发布前需未被占用;之后每次发布必须先 `pnpm version` 升版本(npm 不允许重复发布相同版本)。
38
+ - 在 monorepo 内发布单个子包,务必在包目录(而非仓库根)执行 `pnpm publish`。
@@ -0,0 +1,36 @@
1
+ # 测试
2
+
3
+ Jest 需要 `--experimental-vm-modules` 和 `--no-cache`。请使用 pnpm 脚本,不要直接跑裸 `jest`。本包位于 `wwkit` monorepo 的 `packages/harness`,以下命令需在该目录下执行(或在仓库根用 `pnpm --filter harness run ...`)。
4
+
5
+ ## 单元测试(tests/unit/)
6
+
7
+ ```bash
8
+ # pnpm 命令
9
+ pnpm test -- tests/unit/build-xpath.test.js # 指定单个测试文件
10
+ pnpm run test:all # 整个 tests/unit/ 目录
11
+
12
+ # 直接使用 jest(需 ESM 支持标志;pnpm 下 jest bin 是 shell shim,须指向实际 JS 入口)
13
+ node --experimental-vm-modules node_modules/jest/bin/jest.js tests/unit/build-xpath.test.js --no-cache # 单个文件
14
+ node --experimental-vm-modules node_modules/jest/bin/jest.js tests/unit --no-cache # 整个目录
15
+ ```
16
+
17
+ 单元测试为 ESM(`.js`),直接 import `skills/*/references/*.js` 的导出函数。
18
+
19
+ ## E2E 测试(tests/e2e/,调用 opencode run 实际执行)
20
+
21
+ ```bash
22
+ # pnpm 命令
23
+ pnpm run e2e -- tests/e2e/query/print.test.cjs # 指定单个测试文件
24
+ pnpm run e2e:all # 整个 tests/e2e/ 目录
25
+
26
+ # 直接使用 jest
27
+ node --experimental-vm-modules node_modules/jest/bin/jest.js tests/e2e/query/print.test.cjs --no-cache # 单个文件
28
+ node --experimental-vm-modules node_modules/jest/bin/jest.js tests/e2e --no-cache # 整个目录
29
+ ```
30
+
31
+ E2E 要点(完整约定见 `tests/e2e/README.md`):
32
+
33
+ - E2E 测试为 **CommonJS(`.cjs`)**,禁止写成 ESM `.js`(会触发 jest 的 "Cannot log after tests are done" 异步问题)。
34
+ - 执行真实 `opencode run --auto --pure`,**无断言**,只打印结果需人工判断;`opencode` 必须在 PATH 上。
35
+ - `jest.setup.cjs` 设置 `OPENCODE_CONFIG_DIR=process.cwd()`(加载本仓库插件)与全局 300s 超时;`tests/e2e/helper.cjs` 对子进程有超时 kill 清理。
36
+ - 输出文件一律写入 `tmpdir()`,不污染仓库。
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: extract
3
+ description: |
4
+ 从网页 HTML/文本源码/结构化 JSON 中提取内容,按 format 分发到 references/ 对应流程。
5
+ format=list/detail/text/navi。返回 JSON 数组,不写入任何文件。
6
+ 禁止使用 WebFetch 或任何网络请求获取内容。
7
+ 适用:列表页/详情页/纯文本/结构化 JSON 列表/导航 XPath 等各类网页内容提取。
8
+ license: MIT
9
+ metadata:
10
+ workflow: sequential
11
+ ---
12
+
13
+ # extract 技能
14
+
15
+ ## 输入参数
16
+
17
+ 入参来自调用方(agent)已规整后的字段,字段清单(字段/类型/必填)见 `references/input.schema.json5`。
18
+
19
+ ## 输出
20
+
21
+ 返回符合 `references/{{ format }}.schema.json5` 的 JSON **数组**,**不写入任何文件**。
22
+
23
+ ## 工作流程
24
+
25
+ ### 阶段 1:输入处理
26
+
27
+ - `{{ source }}`:输入内容。若值是现有文件路径,读取文件内容;否则直接以值作为输入。
28
+ - `{{ format }}`:提取类型。当 `format=list` 时,判断 `{{ source }}` 类型:
29
+ - 若能解析为 JSON 数组,使用 `list_from_json` 流程
30
+ - 否则若包含 HTML 标签(形如 `<tag`),作为 HTML 源码,使用 `list_from_html` 流程
31
+ - 否则作为纯文本,使用 `list_from_text` 流程
32
+ - `{{ url }}`:按需透传给对应 format 流程(相对路径转绝对)。
33
+
34
+ 本技能仅使用 `source`、`format`、`url` 三个字段。忽略传入的所有其他字段(如 `count`、`max_length`、`max_image_count`、`language`、`output` 等),不得读取、写入或据此推断任何行为。
35
+
36
+ 若 `{{ source }}` 为空、null 或仅含空白:直接输出 `[]` 并结束,**禁止**继续执行。
37
+
38
+ 若 `{{ format }}` 为空或对应 references 文件不存在:直接输出 `[]` 并结束。
39
+
40
+ ### 阶段 2:加载 references
41
+
42
+ 从技能目录的 `references/` 加载流程文件和 schema 文件:
43
+
44
+ - `format=list` 时,流程文件根据阶段 1 检测结果使用 `references/list_from_html.md`、`references/list_from_json.md` 或 `references/list_from_text.md`;schema 统一使用 `references/list.schema.json5`
45
+ - 其他 format 直接使用 `references/{{ format }}.md` 和 `references/{{ format }}.schema.json5`
46
+
47
+ 用 `read` 工具读取两个文件内容,并**记录 schema 文件的绝对路径**(阶段 4 校验时需要)。若任一文件不存在,则输出 `[]` 并结束。
48
+
49
+ ### 阶段 3:按 format 执行提取
50
+
51
+ 按 `references/` 下对应流程文件的步骤执行提取(`list` 格式使用阶段 1 检测的 `list_from_html`/`list_from_json`/`list_from_text`),得到 JSON 数组。**禁止使用 WebFetch 或任何网络请求获取内容。**
52
+
53
+ ### 阶段 4:Schema 校验
54
+
55
+ 用 `write` 工具将待校验的 JSON 数组写入临时文件,再用 `write` 工具将 `{"data": <JSON 数组>, "schema_path": "<schema 绝对路径>"}` 写入 JSON 输入文件,stdin 重定向交给 Node.js 校验:
56
+
57
+ ```bash
58
+ node '<技能目录>/references/validate-schema.js' < <临时 JSON 输入文件>
59
+ ```
60
+
61
+ - 若校验通过,进入阶段 6
62
+ - 若校验失败,进入阶段 5 进行修复重试(最多 3 次)
63
+
64
+ ### 阶段 5:校验失败时修复重试
65
+
66
+ 当 Schema 校验失败时,将错误信息反馈,重新执行阶段 3 提取并再次校验。**最多重试 3 次**,超过则将最后一次重试的 schema 校验错误信息及「重试 {N} 次后 schema 校验仍然失败」输出到 stderr 并结束。
67
+
68
+ ### 阶段 6:输出
69
+
70
+ **若校验未通过(重试耗尽):** 将最后一次重试的 schema 校验错误信息及「重试 {N} 次后 schema 校验仍然失败」输出到 stderr 并结束。
71
+
72
+ **若校验通过:** 将最终 JSON **数组**作为结果返回。**禁止写入任何文件**;文件写入由调用方(agent)负责。
73
+
74
+ ## 约束
75
+
76
+ - 只处理已声明字段(source/format/url),忽略所有其他传入参数(如 `count`、`max_length`、`max_image_count`、`language`、`output`),**禁止**以任何形式使用它们。
77
+ - 本技能不读取、不写入任何输出文件;仅返回 JSON 数组。
78
+ - **禁止使用 WebFetch 或任何网络请求获取内容**;`{{ source }}` 无正文或为空时必须输出 `[]`,绝不自行获取内容。
79
+ - 忽略参数中除已声明字段之外的所有字段。
80
+ - 内部重试步骤的中间产物禁止写入任何文件。
81
+
82
+ ## 资源目录
83
+
84
+ ```
85
+ references/
86
+ ├── build-xpath.js # navi 的 XPath 构造脚本
87
+ ├── clean-html.js # HTML 清洗
88
+ ├── extract-detail.js # 详情页正文提取(按 img 切分)
89
+ ├── extract-sample.js # 列表页样本截取
90
+ ├── extract-regex.js # 正则批量提取
91
+ ├── validate-schema.js # Schema 校验
92
+ ├── input.schema.json5
93
+ ├── list.schema.json5
94
+ ├── list_from_html.md
95
+ ├── list_from_json.md
96
+ ├── list_from_text.md
97
+ ├── detail.md + detail.schema.json5
98
+ ├── text.md + text.schema.json5
99
+ └── navi.md + navi.schema.json5
100
+ ```
@@ -0,0 +1,31 @@
1
+ import { createInterface } from 'readline';
2
+ import { fileURLToPath } from 'url';
3
+
4
+ const QUOTE_PATTERN = /[\u201c\u201d\u2018\u2019"'"]+/;
5
+
6
+ export function buildXpath(title) {
7
+ const fragments = title.split(QUOTE_PATTERN).map(s => s.trim()).filter(Boolean);
8
+ if (!fragments.length) fragments.push(title);
9
+ const conditions = fragments.map(f =>
10
+ `contains(.,'${f.replace(/'/g, "&apos;")}')`
11
+ ).join(' and ');
12
+ return `//text()[${conditions} and not(ancestor::script)]/..`;
13
+ }
14
+
15
+ function main() {
16
+ const rl = createInterface({ input: process.stdin });
17
+ let raw = '';
18
+ rl.on('line', line => { raw += line; });
19
+ rl.on('close', () => {
20
+ try {
21
+ const items = JSON.parse(raw);
22
+ if (!Array.isArray(items)) { console.log('[]'); return; }
23
+ const result = items
24
+ .filter(item => item.title)
25
+ .map(item => ({ selector: buildXpath(item.title), title: item.title }));
26
+ console.log(JSON.stringify(result, null, 2));
27
+ } catch { console.log('[]'); }
28
+ });
29
+ }
30
+
31
+ if (process.argv[1] === fileURLToPath(import.meta.url)) main();