From 8dc6e5ab06dc676d91f9e3bebb7f1378ecef7371 Mon Sep 17 00:00:00 2001 From: Chen Zheng Date: Mon, 1 Jun 2026 14:37:23 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=84=E8=8C=83=E5=8C=96=E5=90=8E=E7=9A=84sk?= =?UTF-8?q?ill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 19 + SKILL.md | 1032 ++------------------ agents/openai.yaml | 13 + assets/templates/e10-api-doc-template.md | 164 ++++ evals/test-cases.md | 36 + examples/prompt-create-workflow-api-doc.md | 10 + examples/sample-field-extraction.json | 36 + examples/sample-output-api-doc.md | 87 ++ references/internal-apis.md | 306 +++--- references/login-and-browser-harness.md | 144 +++ references/openapi-doc-navigation.md | 155 +++ references/output-document-standard.md | 120 +++ references/troubleshooting.md | 49 + references/workflow-field-discovery.md | 75 ++ 14 files changed, 1107 insertions(+), 1139 deletions(-) create mode 100644 agents/openai.yaml create mode 100644 assets/templates/e10-api-doc-template.md create mode 100644 evals/test-cases.md create mode 100644 examples/prompt-create-workflow-api-doc.md create mode 100644 examples/sample-field-extraction.json create mode 100644 examples/sample-output-api-doc.md create mode 100644 references/login-and-browser-harness.md create mode 100644 references/openapi-doc-navigation.md create mode 100644 references/output-document-standard.md create mode 100644 references/troubleshooting.md create mode 100644 references/workflow-field-discovery.md diff --git a/README.md b/README.md index e69de29..c81926b 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,19 @@ +# fanwei-e10-api-doc + +This skill generates external-facing Markdown API documentation for 泛微 OA E10 OpenAPI integrations. + +It is optimized for workflow-instance API documentation: authenticating to E10 in a read-only browser session, reading OpenAPI documentation, discovering workflow form fields, mapping E10 field types to external payload guidance, and producing a developer-ready Markdown document. + +## Contents + +- `SKILL.md`: routing, workflow, safety boundary, and resource guide. +- `references/`: detailed operational references and documentation standards. +- `assets/templates/`: reusable output template. +- `examples/`: sanitized sample prompt, extracted metadata, and output style. +- `evals/`: trigger and quality checks. + +## Safety + +The skill is explicitly read-only. It must not create, update, delete, publish, submit, approve, or reject any E10 object or data. + +Final documentation must not expose real credentials, tokens, hostnames, database tables, internal API paths, or E10 management implementation details. diff --git a/SKILL.md b/SKILL.md index 311ae1f..584ab38 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,949 +1,91 @@ --- name: fanwei-e10-api-doc -description: 泛微OA E10 系统接口文档撰写 — 自动登录、浏览 API 文档、发现流程字段、生成 MD 文档。 +description: generate external-facing markdown api documentation for 泛微 oa e10 openapi integrations. use when the user asks to write e10 interface documentation, especially for creating workflow instances, discovering workflow form fields, extracting e10 openapi docs, documenting oauth2 access_token flow, or producing developer-ready api docs from an e10 system. --- -# 泛微OA E10 接口文档撰写 - -## ⛔ 硬性约束:只读操作 - -**绝对禁止对 E10 系统进行任何修改操作。** 包括但不限于: -- 不创建、编辑、删除流程定义 -- 不修改表单字段配置 -- 不新增、编辑、删除任何系统数据 -- 不调用任何写操作的 API(POST/PUT/DELETE 等) -- 浏览器操作仅限于导航、查看、截图、提取文本 - -**允许的操作:** -- 查看页面内容 -- 截图记录 -- 提取 API 文档文本 -- 提取流程字段定义 -- 提取表单结构信息 - -如果某个操作看起来可能会修改数据,必须先向用户确认。 - -## Trigger - -用户说"帮我写 E10 的 xxx 接口文档"或"写一个创建 xxx 流程实例的接口文档"时使用。 - -## Phase 0: 获取凭据 - -**每次都向用户确认以下信息,不依赖任何缓存的凭据:** - -- E10 系统地址 (base URL,如 `https://e10.example.com`) -- 账号 -- 密码 - -不要从 memory 中读取凭据,不要缓存凭据到 memory。即使之前用过同一个系统也要重新确认。 - -## Phase 1: 启动独立浏览器 + 登录 - -### ⚠️ 跨平台兼容说明 - -本 skill 在 Linux 和 Windows 上均可使用。下面用变量表示平台差异,Agent 执行时自动替换: - -| 变量 | Linux | Windows | -|------|-------|---------| -| `` | `google-chrome` | `"C:\Program Files\Google\Chrome\Application\chrome.exe"` | -| `` | `/tmp/bh-e10` | `%TEMP%\bh-e10` | -| `` | `/tmp` | `%TEMP%` | -| `` | `pkill -f browser_harness.daemon 2>/dev/null; rm -f /tmp/bu-*.sock /tmp/bu-*.pid 2>/dev/null` | `taskkill /F /IM python.exe /FI "WINDOWTITLE eq browser_harness*" 2>nul & del /Q %TEMP%\bu-*.sock %TEMP%\bu-*.pid 2>nul` | - -### Step 0: 启动独立 Chrome(必须) - -**绝对不要连接用户的日常浏览器**。每次使用前启动一个独立的 headless Chrome 实例。 - -```bash -# 1. 清理旧 daemon(关键!daemon 跨命令持久存活,残留的会连到用户浏览器) - - -# 2. 启动独立 headless Chrome - --headless=new --remote-debugging-port=9223 --user-data-dir= --no-first-run --no-default-browser-check --window-size=1920,1080 - -# 3. 确认端口就绪 -# Linux/macOS: sleep 3 && curl -s http://127.0.0.1:9223/json/version | grep webSocketDebuggerUrl -# Windows: 等待 3 秒后浏览器访问 http://127.0.0.1:9223/json/version 确认有响应 -``` - -启动 Chrome 用 `terminal(background=true)`,之后**每个** browser-harness 命令都必须: - -```bash -export BU_CDP_URL=http://127.0.0.1:9223 && browser-harness <<'PY' -... -PY -``` - -> **严禁分开执行** `export BU_CDP_URL=...` 然后 `browser-harness` — daemon 是独立进程,不会继承后续 shell 的环境变量。必须 `export && browser-harness` 在同一行。Windows 用 `set BU_CDP_URL=http://127.0.0.1:9223 && browser-harness`。 - -### Step 1: 导航到登录页 - -```bash -export BU_CDP_URL=http://127.0.0.1:9223 && browser-harness <<'PY' -new_tab("https:///login") -wait_for_load() -capture_screenshot("/e10_login.png") - -# 确认在登录页 -url = js("window.location.href") -print("URL:", url) -# 如果 URL 不是 /login 而是其他页面(如 /attend/info),说明之前的 session 还在, -# 这是正常的 — 直接用即可,跳到 Phase 3C -PY -``` - -### Step 2: 识别验证码 + 登录 - -如果 Step 1 已在登录页,继续登录;如果已登录(URL 不含 `/login`),跳到 Phase 3C。 - -**验证码规则**:E10 图形验证码**始终为 4 位数字**,不会出现 3 位或以下。 - -```bash -export BU_CDP_URL=http://127.0.0.1:9223 && browser-harness <<'PY' -# 截图识别验证码 -capture_screenshot("/e10_captcha.png") -PY -``` - -用 `vision_analyze` 识别截图。**如果返回少于 4 位**(识别不全或 headless 渲染空白),则点击验证码图片刷新后重新截图: - -```bash -export BU_CDP_URL=http://127.0.0.1:9223 && browser-harness <<'PY' -import json - -# 点击验证码图片刷新 -info = json.loads(js(''' -var imgs = document.querySelectorAll("img"); -for (var i = 0; i < imgs.length; i++) { - var r = imgs[i].getBoundingClientRect(); - if (r.width > 20 && r.height > 20) { - // 验证码图片通常在输入框右侧,尺寸约 120x30 - return JSON.stringify({x: Math.round(r.left+r.width/2), y: Math.round(r.top+r.height/2)}); - } -} -// fallback: 第三个 input 右侧 180px -var inputs = document.querySelectorAll("input"); -var r = inputs[2].getBoundingClientRect(); -return JSON.stringify({x: Math.round(r.right + 90), y: Math.round(r.top + r.height/2)}); -''')) -click_at_xy(info["x"], info["y"]) -wait(2) -capture_screenshot("/e10_captcha.png") -PY -``` - -拿到 4 位验证码后,用 native setter + 事件派发登录: - -```bash -export BU_CDP_URL=http://127.0.0.1:9223 && browser-harness <<'PY' -import json - -# 获取 input 坐标(每次都重新获取,data-id 在页面刷新后会变) -info = json.loads(js(''' -var inputs = document.querySelectorAll("input"); -var result = []; -for (var i = 0; i < inputs.length; i++) { - var r = inputs[i].getBoundingClientRect(); - result.push({ - dataId: inputs[i].getAttribute("data-id"), - x: Math.round(r.left + r.width/2), - y: Math.round(r.top + r.height/2) - }); -} -var btn = document.querySelector("button"); -var br = btn ? btn.getBoundingClientRect() : null; -return JSON.stringify({inputs: result, btn: br ? {x: Math.round(br.left+br.width/2), y: Math.round(br.top+br.height/2)} : null}); -''')) - -u = info["inputs"][0]; p = info["inputs"][1]; c = info["inputs"][2]; b = info["btn"] - -# 注入 fillInput -js(''' -var ns = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; -window.__fi = function(dataId, value) { - var el = document.querySelector("[data-id='" + dataId + "']"); - el.focus(); el.value = ""; - ns.call(el, value); - el.dispatchEvent(new Event("input", { bubbles: true })); - el.dispatchEvent(new Event("change", { bubbles: true })); - el.dispatchEvent(new Event("blur", { bubbles: true })); -}; -''') - -click_at_xy(u["x"], u["y"]); wait(0.2) -js('window.__fi("' + u["dataId"] + '", "")') -click_at_xy(p["x"], p["y"]); wait(0.2) -js('window.__fi("' + p["dataId"] + '", "")') -click_at_xy(c["x"], c["y"]); wait(0.2) -js('window.__fi("' + c["dataId"] + '", "")') - -# 验证字段都有值再登录 -vals = js(''' -var u = document.querySelector("[data-id='" + u["dataId"] + "']"); -var p = document.querySelector("[data-id='" + p["dataId"] + "']"); -var c = document.querySelector("[data-id='" + c["dataId"] + "']"); -return JSON.stringify({user: u?u.value:"?", pass: p?"***":"?", captcha: c?c.value:"?"}); -''') -print("Values:", vals) - -wait(0.3) -click_at_xy(b["x"], b["y"]) -wait(5) -wait_for_load() -url = js("window.location.href") -print("After login:", url) -# 如果仍在 /login,验证码错误或登录失败,重新识别验证码 -PY -``` - -登录成功后,后续所有 `browser-harness` 命令都继续使用 `export BU_CDP_URL=http://127.0.0.1:9223 &&` 前缀。 - -### 清理 - -任务完成后,杀 headless Chrome 释放资源: - -```bash -# Linux/macOS -pkill -f "chrome.*9223" 2>/dev/null -# Windows -taskkill /F /IM chrome.exe 2>nul -``` - -## Phase 2: 确定文档类型 - -### Case A — 通用接口文档 - -用户没提到具体流程名称(如"帮我写 E10 的人员同步接口"),直接跳到 Phase 3A。 - -### Case B — 流程实例接口文档 - -用户提到具体流程名称(如"帮我写创建采购申请流程实例的接口文档"),先执行 Phase 3B 发现流程字段,再执行 Phase 3A 查阅 API 文档。 - -## Phase 3A: 浏览 API 文档主页 - -> 以下所有 `browser-harness` 命令都需要 `export BU_CDP_URL=http://127.0.0.1:9223 &&` 前缀,为简洁省略。 - -导航到 `/sp/openapi/base/doc/index`(React SPA,内容由 JS 动态渲染)。 - -```bash -new_tab("https:///sp/openapi/base/doc/index") -wait(5) -wait_for_load() -wait(3) -capture_screenshot("/tmp/e10_api_doc_home.png") -PY -``` - -### 获取所有 API 文档链接 - -文档首页使用 `` 标签组成的网格。用 JS 提取所有链接,然后直接 `new_tab()` 导航到目标文档页: - -```bash -browser-harness <<'PY' -import json -links = json.loads(js(''' -var all = document.querySelectorAll("a"); -var result = []; -for (var i = 0; i < all.length; i++) { - var a = all[i]; - var txt = a.textContent.trim(); - if (txt.length > 0 && txt.length < 30 && a.href) { - result.push({text: txt, href: a.href}); - } -} -return JSON.stringify(result); -''')) -for l in links: - print(l["text"], "->", l["href"]) -PY -``` - -找到目标分类(如"工作流程")的 href,直接导航: - -```bash -browser-harness <<'PY' -new_tab("") # 如 /sp/opendoc/freepass/10.0.2508.01/zh_cn/840714220321120257 -wait(8) # React SPA 需要更长时间渲染 -wait_for_load() -wait(3) -PY -``` - -> **注意**:这些文档页是 React SPA,`curl` 只能拿到空壳 HTML(`
`)。必须用浏览器渲染后通过 `js("document.body.innerText")` 提取内容。 - -### 提取文档内容 - -文档是纯文本格式,用缩进表示参数层级。直接抓取全部 body 文本: - -```bash -browser-harness <<'PY' -content = js("document.body.innerText.substring(0, 15000)") -print(content[:5000]) -PY -``` - -如果内容被截断,滚动页面再提取后续部分: - -```bash -browser-harness <<'PY' -js("window.scrollBy(0, 800)") -wait(2) -content = js("document.body.innerText.substring(8000, 15000)") -print(content[:5000]) -PY -``` - -**⚠️ 注意接口名称**:流程相关的接口有两个容易混淆: -- **"新增流程"** — 创建流程**定义/模板**(管理员用,不要看这个) -- **"创建流程实例" / "工作流程"** — 创建流程**实例**(外部系统调用,看这个) - -文档分类中"工作流程"下的内容才是外部系统需要的流程实例创建接口。 - -## Phase 3B: 流程发现与字段检查 - -### Step 1: 查看所有流程 - -导航到 `/info/engine_wf/pathdef/list/company`。 - -```bash -browser-harness <<'PY' -new_tab("https:///info/engine_wf/pathdef/list/company") -wait(5) -wait_for_load() -wait(3) -capture_screenshot("/tmp/e10_workflows.png") -PY -``` - -页面展示流程卡片列表。确认目标流程是否存在。 - -### Step 2: 点击目标流程卡片(用坐标点击) - -JS `click()` 在流程卡片上经常失效。用坐标点击代替: - -```bash -browser-harness <<'PY' -import json -# 找到 "06.用车审批流程" 卡片的精确坐标 -info = json.loads(js(''' -var all = document.querySelectorAll("*"); -var result = []; -for (var i = 0; i < all.length; i++) { - var el = all[i]; - var txt = el.textContent.trim(); - // 找叶子节点(children.length === 0)且文本匹配 - if (txt === "06.用车审批流程" && el.children.length === 0) { - // 向上找卡片容器(width > 100, height > 40) - var p = el; - while (p && p.tagName !== "BODY") { - var r = p.getBoundingClientRect(); - if (r.width > 100 && r.height > 40) break; - p = p.parentElement; - } - if (p) { - var r = p.getBoundingClientRect(); - result.push({ - x: Math.round(r.left + r.width/2), - y: Math.round(r.top + r.height/2), - w: Math.round(r.width), h: Math.round(r.height) - }); - } - } -} -return JSON.stringify(result); -''')) -# 用坐标点击 -click_at_xy(info[0]["x"], info[0]["y"]) -wait(5) -wait_for_load() -print("URL:", js("window.location.href")) -capture_screenshot("/tmp/e10_workflow_detail.png") -PY -``` - -成功后将跳转到 `/info/engine_wf/pathdef/list/company/pathset//base`。URL 中的数字即为该流程的 workflowId。 - -### Step 3: 进入字段管理(多级标签页导航) - -流程详情页右上角标签栏结构: -``` -[基础设置] [功能设置] [表单管理] [布局管理] [更多] -``` - -点击 **"表单管理"** 标签(坐标约 y=38),URL 会变为 `.../formManager`。 - -然后在 **"表单管理"** 页面内,还有一组子标签: -``` -[基础信息] [字段设置] [数据设置] [布局设置] [其他设置] -``` - -以及另一组功能标签: -``` -[字段管理] [字段查重校验] [附件设置] [字段显示格式] [自定义e-code开发] [索引管理] -``` - -点击 **"字段管理"**(坐标约 y=74 的第二组标签),才能看到真正的字段列表。 - -> **注意**:左边栏菜单也有一个"字段管理",那是全局入口,坐标在 y≈58。不要点错 — 要点详情页内 y≈74 的那个。 - -```bash -browser-harness <<'PY' -import json -# Step 3a: 点击"表单管理"tab -info = json.loads(js(''' -var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false); -var result = []; -while (walker.nextNode()) { - var el = walker.currentNode; - if (el.textContent.trim() === "表单管理" && el.children.length === 0) { - var r = el.getBoundingClientRect(); - result.push({x: Math.round(r.left + r.width/2), y: Math.round(r.top + r.height/2), w: r.width, h: r.height}); - } -} -return JSON.stringify(result); -''')) -# 选 y 坐标较大的那个(y>100 的是页面标签,y<100 的是侧边栏) -tab = [p for p in info if p["y"] > 30][0] if len(info) > 1 else info[0] -click_at_xy(tab["x"], tab["y"]) -wait(5) -wait_for_load() - -# Step 3b: 点击"字段管理"子标签 -info2 = json.loads(js(''' -// 同上找 y≈74 的元素 -var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false); -var result = []; -while (walker.nextNode()) { - var el = walker.currentNode; - if (el.textContent.trim() === "字段管理" && el.children.length === 0) { - var r = el.getBoundingClientRect(); - result.push({x: Math.round(r.left + r.width/2), y: Math.round(r.top + r.height/2)}); - } -} -return JSON.stringify(result); -''')) -# 选 y 在 60-120 之间的(字段管理子标签),排除侧边栏菜单的 -subtab = [p for p in info2 if 60 < p["y"] < 120][0] -click_at_xy(subtab["x"], subtab["y"]) -wait(5) -capture_screenshot("/tmp/e10_fields_page1.png") -PY -``` - -### Step 4: 提取字段信息 + 切换主表/明细表 - -字段列表有三个子标签:**主表 | 明细表 | 其他**。默认显示主表。 - -**推荐方法:用内部 API 获取字段(Phase 3C)**,避免分页和 DOM 提取问题。DOM 提取方法仅在 Phase 3C 不可用时作为备选。 - -## Phase 3C: 通过内部 API 获取精确字段数据(推荐) - -> 以下所有 `browser-harness` 命令都需要 `export BU_CDP_URL=http://127.0.0.1:9223 &&` 前缀,为简洁省略。登录后 browser session cookie 自动携带。 - -> 完整的 API 列表和响应格式见 `references/internal-apis.md`。 - -### 完整链路(4 步,无需任何 UI 点击) - -登录后直接按以下顺序调用,全部走 `fetch()`: - -#### Step 1: 获取所有流程 → 找到目标 workflowId - -``` -POST /api/bs/workflow/pathdef/baseSet/getBaseInfoListTree -``` - -搜索目标流程名称(如用户指定"用车审批流程"),从返回的树形结构中获取 `workflowId`。 - -```bash -browser-harness <<'PY' -import json - -result = json.loads(js(''' -(async () => { - let resp = await fetch("/api/bs/workflow/pathdef/baseSet/getBaseInfoListTree", { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({ - searchParams: { otherSearchDatas: { workflowType: "", workflowId: "", subCompanyId: "" } }, - isTemplate: 0, - belongType: 1 - }) - }); - return JSON.stringify(await resp.json()); -})(); -''')) - -# 按名称匹配目标流程 -target_name = "用车审批流程" -for group in result["data"]["datas"]: - for child in group["children"]: - if target_name in child["title"]: - wf_id = child["id"] - print(f"找到: {child['title']} | workflowId={wf_id} | 分类={group['typeName']}") -PY -``` - -#### Step 2: 搜索表单 → 获取 formId - -``` -POST /api/workflow/core/form/formmanage/getFormList -``` - -表单名称可能与流程名称不同(如流程叫"06.用车审批流程",表单叫"用车审批流程")。用流程名称的**核心关键词**搜索。 - -```bash -browser-harness <<'PY' -import json - -result = json.loads(js(''' -(async () => { - let resp = await fetch("/api/workflow/core/form/formmanage/getFormList", { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({ module: "workflow", pageNo: 1, pageSize: 50, name: "用车" }) - }); - return JSON.stringify(await resp.json()); -})(); -''')) - -for f in result["data"]["pageDatas"]["result"]: - if target_name in f.get("name", ""): - form_id = f["id"] - print(f"表单: {f['name']} | formId={form_id}") -PY -``` - -> **备选**:如果 `getFormList` 不可用,可从 `localStorage` 提取 formId。访问表单管理页面后,`localStorage` 会有 key `Form_0__checkNewForm`。 - -#### Step 3: 获取表单详情 → 确认表名和类型 - -``` -POST /api/workflow/core/form/formmanage/getForm -``` - -```bash -browser-harness <<'PY' -import json - -result = json.loads(js(''' -(async () => { - let resp = await fetch("/api/workflow/core/form/formmanage/getForm", { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({ module: "workflow", form: { id: "" } }) - }); - return JSON.stringify(await resp.json()); -})(); -''')) -data = result["data"] -print(f"表名: {data['tableName']} | 类型: {data['tableType']} | 名称: {data['name']}") -# 检查是否有明细表 -dts = data.get("detailTable", []) -print(f"明细表数量: {len(dts)}") -for dt in dts: - print(f" 明细表: {dt.get('name')} | id: {dt.get('id')}") -PY -``` - -#### Step 4: 获取字段列表 → 全部字段一次拿到 - -``` -POST /api/workflow/core/form/field/manage/getFormFieldPage -``` - -```bash -browser-harness <<'PY' -import json - -result = json.loads(js(''' -(async () => { - let resp = await fetch("/api/workflow/core/form/field/manage/getFormFieldPage", { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({ - pageNo: 1, - pageSize: 20, - module: "workflow", - formFieldSearchEntity: { - isDelete: 0, - status: "enable", - formId: "", - formTableId: "" - } - }) - }); - return JSON.stringify(await resp.json()); -})(); -''')) - -records = result["data"]["pageDatas"]["result"] -print(f"主表字段: {len(records)} 个") -for r in records: - print(f" {r['title']} | key={r['dataKey']} | type={r['type']} | fieldId={r['fieldId']} | sort={r['showOrder']} | group={r['groupName']}") -PY -``` - -> **重要参数细节**:`pageNo` 不是 `page`,`pageSize` 不是 `size`。`formFieldSearchEntity` 是必传嵌套对象,至少含 `formId` 和 `isDelete: 0`。主表字段需额外传 `formTableId`。 - -#### Step 5: 获取 formTableId + 明细表字段 - -**主表 formTableId** 从 Step 4 返回的任意字段中提取(每个字段都有 `formTableId` 属性)。 - -**明细表**:需要获取明细表的 `formTableId`。方法: -- 先通过 UI 点击切换一次 明细表 标签,同时用 XHR 拦截捕获请求体中的 `formTableId`(见下方 Fallback) -- 如果页面显示"您暂时还没有明细表",说明该流程无明细表 - -**XHR 拦截获取 formTableId(Fallback)**: - -```bash -browser-harness <<'PY' -import json - -# 注入 XHR 拦截器 -js(''' -window.__BH_XHR = []; -var OrigXHR = window.XMLHttpRequest; -window.XMLHttpRequest = function() { - var xhr = new OrigXHR(); - var origOpen = xhr.open, origSend = xhr.send; - xhr.open = function(m, u) { this.__url = u; return origOpen.apply(this, arguments); }; - xhr.send = function(b) { window.__BH_XHR.push({url: this.__url, body: b}); return origSend.apply(this, arguments); }; - return xhr; -}; -''') - -# 触发 API 调用:鼠标事件点击明细表标签(MouseEvent 比 click_at_xy 更可靠) -js(''' -var divs = document.querySelectorAll("div"); -for (var i = 0; i < divs.length; i++) { - if (divs[i].textContent.trim() === "明细表" && divs[i].className.indexOf("menu") !== -1) { - divs[i].dispatchEvent(new MouseEvent("click", {bubbles: true})); - break; - } -} -''') -wait(4) - -xhr = json.loads(js('JSON.stringify(window.__BH_XHR || [])')) -for r in xhr: - if "getFormFieldPage" in str(r.get("url", "")): - print("Body:", r["body"]) # 从中提取 formTableId -PY -``` - -得到明细表 `formTableId` 后,用 Step 4 相同的 API(换 `formTableId`)即可获取明细表字段。返回空数组则说明明细表为空。这在文档中注明即可,不要报错。 - -#### UI Fallback(仅在 API 不可用时使用) - -如果上述 API 全部不可用(极少数情况),回退到 Phase 3B 的坐标点击 + DOM 提取方案。但优先尝试 API。 - -#### 备选:DOM 提取字段(不推荐) - -```bash -browser-harness <<'PY' -import json -fields = json.loads(js(''' -var rows = document.querySelectorAll("table tr"); -var result = []; -for (var i = 0; i < rows.length; i++) { - var cells = rows[i].querySelectorAll("td"); - if (cells.length >= 5) { - var rowData = []; - for (var j = 0; j < cells.length; j++) { - rowData.push(cells[j].textContent.trim()); - } - result.push(rowData); - } -} -return JSON.stringify(result); -''')) -print("Total:", len(fields)) -for row in fields: - print("|", " | ".join(row[:8]), "|") -PY -``` - -**切换明细表**:找文本为"明细表"的 SPAN 元素,点击其父 DIV: - -```bash -browser-harness <<'PY' -# 先确保"明细表"tab 不处于 active 状态(避免重复点击) -import json -active = json.loads(js(''' -var el = document.querySelector("[class*='ui-menu-list-item-active']"); -return el ? el.textContent.trim() : "none"; -''')) -# 如果当前是"主表"active,切换到"明细表" -if active == "主表": - js(''' - var divs = document.querySelectorAll("div"); - for (var i = 0; i < divs.length; i++) { - if (divs[i].textContent.trim() === "明细表" && divs[i].className.indexOf("menu") !== -1) { - divs[i].dispatchEvent(new MouseEvent("mousedown", {bubbles: true})); - divs[i].dispatchEvent(new MouseEvent("mouseup", {bubbles: true})); - divs[i].dispatchEvent(new MouseEvent("click", {bubbles: true})); - return true; - } - } - return false; - ''') - wait(3) -capture_screenshot("/tmp/e10_detail_table.png") -# 再次提取字段 -fields = json.loads(js('''...''')) # 同上 -PY -``` - -明细表可能为空(显示"您暂时还没有明细表"),这是正常的。在文档中注明即可。 - -## Phase 4: Token 获取(如 API 需要) - -E10 OpenAPI 使用 **OAuth2 code 换 access_token** 模式(非 Bearer token header)。 - -### Step 1: 获取 code - -- **URL**: `POST /papi/openapi/oauth2/authorize` -- **Method**: POST -- **Content-Type**: application/json - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| corpid | String | 是 | 企业 corpId,开发者资料中查看 | -| response_type | String | 是 | 固定为 `code` | -| state | String | 是 | 自定义参数,a-zA-Z0-9,最长128字节 | - -**响应**: `{"errcode": "0", "code": "..."}` — code 有效期 10 分钟且只能使用一次。 - -### Step 2: code 换 access_token - -access_token 作为请求参数(**不是 Header**)传入 API,例如: -```json -{"access_token": "210b03...7c7a", "userid": 123, "workflowId": "456"} -``` - -Access_token 交换接口在"认证"分类下的独立子页面(文档 ID 与获取 code 不同)。从"对接说明"页面可找到"获取accessToken信息"链接。接口详情: -- **URL**: `POST /papi/openapi/oauth2/access_token` -- **参数**: app_key(应用 key), app_secret(应用密钥), grant_type="authorization_code", code -- **响应**: `{"errcode":"0", "accessToken":"...", "refreshToken":"...", "expires_in":7200}` -- access_token 有效期 2 小时 - -在输出 MD 的"前置条件"章节中写入 OAuth2 两步流程。 - -## Phase 5: 文件上传(如 API 涉及文件) - -如果目标 API 接受文件参数(附件字段),需要查阅文件上传接口。文件上传文档在 **"知识文档" → "文件上传"** 分类下,不是独立的一级入口。 - -从 Phase 3A 获取的链接列表中找"知识文档"或"文件上传"相关链接,直接 `new_tab()` 导航获取上传 API 规范。 - -常见上传模式: -- 先调用文件上传接口获取 `fileId` / `attachmentId` -- 再在业务 API 的 `formData.dataDetails` 中通过 `dataOptions` 引用(带 `uploadParam` 和 `uploadType: "loadUrl"`) - -在输出 MD 中加入"前置条件 — 文件上传"章节。 - -## Phase 6: 生成 MD 文档 - -### 文档定位 - -这份文档 **99% 的读者是外部公司的开发人员**,他们只关心怎么调接口、传什么参数、返回什么。他们不关心也不该看到 E10 系统内部的任何信息。 - -### 文档规范(必读) - -**URL 规范**: -- 所有示例 URL 使用占位符 `https://`,**禁止出现真实 IP 地址** -- `access_token` 等其他敏感值也使用占位符 `""`、`""` 等 - -**禁止出现的内容**: -- E10 系统版本号(如"10.0.2508.01") -- 数据库表名(如 `ft_hysydlc`)— 外部调用者不需要知道 -- 内部 API 路径(如 `/api/bs/workflow/...`)— 这些不是 OpenAPI -- E10 内部管理界面的操作说明(如"登录开放平台") -- "未分组"、"辅助信息" 等 E10 界面的内部分组名 — 字段按逻辑含义分组 -- Python urlencode `+` 号替换 `%20` — 这是内部已知的 quirks,不是 API 规范 -- "数据来源"、"文档生成时间" 等元信息 -- 任何"注意"、"提示"、"陷阱" 等口语化标记 — 用规范的表格或说明代替 -- 任何 E10 内部系统的名词和操作流程 - -**必须包含**: -- 完整的鉴权流程(OAuth2 code → access_token),包括每个步骤的 URL、参数、响应 -- 鉴权方式说明:access_token 作为**请求体参数**传入,不是 HTTP Header -- **身份标识转换章节**(非常关键):外部系统不持有 E10 内部 ID,必须说明如何用工号、部门编号等外部标识替代内部 ID。包括: - - 顶层 `userType` 参数:JOB_NUM / EMAIL / MOBILE / idNos / loginID / account - - 顶层 `deptType` 参数:DEPT_CODE / DEPT_NAME - - 表单 dataOptions 中的 `userType` / `deptType` 用法(Employee/Department 类型字段) -- 每个参数的类型、必填、说明 -- curl 请求示例(至少 2 个:最小请求 + 完整请求),**示例中必须使用 userType/DEPT_CODE 等外部标识而非内部 ID** -- 响应示例和错误码 - -**字段列表规范**: -- 不写内部 API 返回的 groupName(如"未分组"、"基础信息"),按字段的实际业务含义分组 -- 字段表头统一:`| 字段名 | 类型 | 必填 | 说明 |` -- 关联型字段在"说明"列注明取值方式(如"dataOptions 传值,type=resource") -- 如果流程无明细表,直接省略明细表章节,不要写"无明细表" - -**模板**: - -```markdown -# <接口名称> - -## 接口说明 -<一句话描述接口用途> - -## 前置条件 - -### 获取 access_token - -#### Step 1: 获取 code - -| 参数 | 必填 | 类型 | 说明 | -|------|------|------|------| -| ... | ... | ... | ... | - -```json -// 请求 -{ ... } -// 响应 -{ ... } -``` - -#### Step 2: code 换 access_token - -| 参数 | 必填 | 类型 | 说明 | -|------|------|------|------| -| ... | ... | ... | ... | - -(如果有文件上传需求,在此加 "### 文件上传" 章节) - -## 请求信息 - -- **URL**: `POST https:///papi/openapi/xxx` -- **Content-Type**: `application/json` - -## 请求参数 - -| 参数名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| access_token | String | 是 | 接口调用凭证(请求体参数,不是 Header) | -| ... | ... | ... | ... | - -## 请求示例 - -### 最小请求 - -```bash -curl -X POST "https:///papi/openapi/xxx" \ - -H "Content-Type: application/json" \ - -d '{ ... }' -``` - -### 完整请求 - -```bash -curl -X POST "https:///papi/openapi/xxx" \ - -H "Content-Type: application/json" \ - -d '{ ... }' -``` - -## 响应参数 - -| 参数名 | 类型 | 说明 | -|--------|------|------| -| errcode | String | 返回码,"0" 表示成功 | -| ... | ... | ... | - -## 响应示例 - -### 成功 - -```json -{ ... } -``` - -### 失败 - -```json -{ ... } -``` - -## 错误码 - -| 错误码 | 说明 | -|--------|------| -| 0 | 成功 | -| ... | ... | - -## 流程字段说明 - -### <字段分组名> - -| 字段名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| ... | ... | ... | ... | -``` - -文件保存到用户指定的目录,文件名 `E10_<流程名称>_API.md`。 - -## Pitfalls - -0. **API 优先,UI 兜底**:登录后能用内部 API 解决的就别去点 UI。页面有多级标签、坐标点击不稳定、分页控件难操作。推荐链路:`getBaseInfoListTree` → `getFormList` → `getForm` → `getFormFieldPage`。详见 `references/internal-apis.md`。 - -1. **SPA 渲染延迟**:E10 是动态渲染页面,每次导航后必须 `wait(3-5)` 再截图/提取内容。API 文档页是 React SPA,需要 `wait(8)` 以上才能渲染完成。 - -2. **登录必须用 native setter + 事件派发**:E10 表单框架不响应直接 `js().value = "..."` 赋值。必须:点击输入框获得焦点 → `Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set` 设值 → 派发 `input`/`change`/`blur` 事件。 - -3. **用 `data-id` 定位 input**:E10 的 input 元素没有有意义的 name/placeholder,但每个都有唯一的 `data-id`(如 `ui-input-fxv2kf`)。先 `js()` 提取所有 input 的 data-id,再用 `[data-id='xxx']` 选择器。 - -4. **流程卡片点击用坐标不用 JS click**:`el.closest("[class*=card]").click()` 在流程列表卡片上经常失效。用树遍历找到叶子文本节点 → 向上找容器 → `getBoundingClientRect()` → `click_at_xy()`。 - -5. **多级标签页导航容易点错**:流程详情页的"字段管理"入口不在侧边栏菜单,而在页面内部的多级标签中: - - 一级标签:基础设置 | 功能设置 | **表单管理** | 布局管理 | 更多 - - 点击"表单管理"后 → 二级子标签:字段管理 | 字段查重校验 | 附件设置 | ... - - 点击"字段管理"后 → 三级标签:**主表** | **明细表** | 其他 - - 左右两侧都有"表单管理"/"字段管理"菜单,坐标不同,要点右侧详情面板内的 - -6. **"字段设置" ≠ "字段管理"**:两个子标签名称极其相似,但功能完全不同。"字段设置"只有三个开关(系统设置/自定义),"字段管理"才是真正的字段列表。不要点错。 - -7. **分页提取不可靠**:字段列表的分页控件用 JS click 和坐标点击都可能失效。**直接用 API 的 `pageSize=20` 一次拿全部字段,不要跟分页控件较劲。** - -8. **API 文档链接在 `` 标签 href 中**:`/sp/openapi/base/doc/index` 的每个分类入口是 ``。用 `js()` 提取所有链接后 `new_tab()` 直接导航,不要尝试 JS click。 - -9. **文档页是纯文本格式**:提取内容用 `js("document.body.innerText")` 获取全部文本。参数层级用缩进区分(类似 YAML),用 `scrollBy()` 分段提取长文档。 - -10. **OAuth2 文档分两页,不在同一页面**:"认证"分类下有两个独立子页面: - - 获取 code:文档 ID 不同于获取 token 的页面 - - 获取 accessToken:另一个文档 ID - 从"对接说明"页面可找到两个链接。不要以为"认证"一个页面就包含完整流程,找不到第二页就去"对接说明"找链接。两个接口: - - Step 1: `POST /papi/openapi/oauth2/authorize`(code 有效期 10 分钟) - - Step 2: `POST /papi/openapi/oauth2/access_token`(参数 app_key, app_secret, grant_type="authorization_code", code) - Token 是 OAuth2 模式,不是 Bearer。access_token 作为请求参数(不是 Header)传入。 - -11. **登录失败静默检测**:登录后务必检查 URL 是否跳转到主页而非留在 login 页面,否则后续所有操作都会拿到登录页 HTML。 - -11b. **验证码固定 4 位数,少于 4 位就是识别不全**:E10 验证码始终 4 位数字。`vision_analyze` 可能漏读。如果返回少于 4 位,点击验证码图片刷新后重新截图识别。填完所有字段后、点击登录前,用 `js()` 验证三个 input 的 value 都有值。 - -12. **明细表检测优先用 getForm API**:`getForm` 返回的 `data.detailTable` 数组直接告诉你是否有明细表(空数组 = 无明细表)。比 Phase 3B 的 XHR 拦截 + UI 点击方案更简单可靠。XHR 方案仅作为备选。 - -13. **内部 API 用 XHR 不是 fetch**:E10 前端调用内部 API 用的是 `XMLHttpRequest`。如果要拦截请求体,必须 hook `XMLHttpRequest.prototype.send`。 - -14. **内部 API 参数命名不同**:`pageNo`(不是 `page`)、`pageSize`(不是 `size`)。`formFieldSearchEntity` 是必传嵌套对象,至少含 `formId` 和 `isDelete: 0`。 - - **formTableId 陷阱**:主表查询时传 `formTableId` 可能返回 0 条记录,**先不传 formTableId 尝试**。 - -15. **workflowId ≠ formId**:工作流 ID(pathSetId)和表单 ID 是不同的实体。`getBaseInfoListTree` 返回的是 workflowId,`getFormList` 返回的是 formId。两者不能混用。 - -16. **表单名称与流程名称可能不同**:流程名可能带编号前缀(如"06.用车审批流程"),表单名可能不带(如"用车审批流程")。搜索表单时用核心关键词模糊匹配。 - -17. **"新增流程" ≠ "创建流程实例"**:API 文档中这两个是不同的接口。"新增流程"是创建流程定义/模板(管理员用),外部系统调用要看"工作流程"分类下的流程实例创建接口。 - -18. **文件上传文档在"知识文档"分类下**:不是独立的顶级入口。从文档首页链接列表中找到"知识文档"或"文件上传"即可。 - -19. **身份标识转换是必写章节**:外部系统的开发人员不知道 E10 内部的用户 ID / 部门 ID。文档必须包含独立的"身份标识转换"章节,说明 `userType`(JOB_NUM/EMAIL/MOBILE/idNos/loginID/account)和 `deptType`(DEPT_CODE/DEPT_NAME)的用法。请求示例中全部使用外部标识(如工号 `"EMP001"`、部门编号 `"DEPT_TECH"`),不要出现 `` 这样的占位符。 - -20. **daemon 持久存活 + 环境变量陷阱**:`browser-harness` 的 daemon 是独立进程,跨命令存活。`export BU_CDP_URL=...` 然后下一行 `browser-harness` 不会生效。必须写成一行:`export BU_CDP_URL=http://127.0.0.1:9223 && browser-harness`。每次新开任务前清理旧 daemon。 - -21. **headless 截图可能首次空白**:headless Chrome 的 CSS 背景图可能首次渲染不完整,`vision_analyze` 返回"纯白"。此时重新 `capture_screenshot` 再识别即可。 - -22. **CloakBrowser 不兼容**:CloakBrowser 是 Playwright 封装,不走 CDP 协议,无法与 browser-harness 对接。 - -23. **Windows 兼容**:本 skill 分享给同事时,Windows 上需调整:① Chrome 路径 `"C:\Program Files\Google\Chrome\Application\chrome.exe"`,② 临时目录用 `%TEMP%`,③ 环境变量用 `set` 而非 `export`,④ `pkill`/`rm` 换成 `taskkill`/`del`。 +# Fanwei E10 API Documentation + +Use this skill to create developer-facing Markdown API documentation for 泛微 OA E10 OpenAPI integrations. Focus on external system developers: how to authenticate, which endpoint to call, what parameters to send, how workflow form fields map to request payloads, and how to handle responses. + +## Hard boundary: read-only operation + +Never modify the E10 system while using this skill. + +Allowed actions: +- Navigate pages, view content, take screenshots, and extract rendered text. +- Use authenticated browser-session read calls to retrieve workflow, form, and field metadata. +- Inspect E10 OpenAPI documentation pages. +- Generate documentation outside the E10 system. + +Forbidden actions: +- Create, update, delete, publish, enable, disable, submit, approve, or reject any workflow, form, system object, or business data. +- Call any endpoint or UI action that changes E10 configuration or data. +- Save credentials to memory, files, logs, examples, or final documentation. + +If an endpoint or UI action might write data, stop and ask the user before continuing. + +## Required inputs + +Before accessing E10, collect or confirm: +- E10 base URL, expressed as `https://` in final documentation. +- Login account and password for the current task only. +- Target documentation type: general E10 OpenAPI documentation or workflow-instance API documentation. +- Target workflow name when documenting workflow instance creation. +- Desired output location and filename if the user cares; otherwise save as `E10__API.md`. + +Do not reuse cached credentials. Do not expose real IPs, real credentials, real tokens, internal IDs, database names, or internal API paths in the final external-facing document unless the user explicitly requests an internal-only diagnostic note. + +## Workflow decision tree + +1. Determine whether the request is for a general E10 API or for creating a workflow instance. +2. If the task needs live E10 inspection, read `references/login-and-browser-harness.md` and log in through an isolated browser session. +3. For OpenAPI documentation pages, read `references/openapi-doc-navigation.md` and extract rendered documentation text. +4. For workflow-instance documentation, read `references/workflow-field-discovery.md` and use the internal read-only API chain described in `references/internal-apis.md` to discover workflow, form, and field metadata. +5. If the API involves attachments, read the file-upload section in `references/openapi-doc-navigation.md` and include an upload prerequisite section. +6. Generate the final Markdown using `assets/templates/e10-api-doc-template.md`. +7. Validate the output with `references/output-document-standard.md` and `evals/test-cases.md` before presenting or saving it. +8. If live access fails, provide the missing inputs or evidence needed instead of inventing field definitions, endpoint details, owners, dates, IDs, or examples. + +## External documentation rules + +The final output must be written for external integration developers, not E10 administrators. + +Always include: +- OAuth2 `code` to `access_token` flow. +- A clear statement that `access_token` is passed as a request-body parameter, not as an HTTP Bearer header. +- Identity mapping guidance: `userType`, `deptType`, and field-level `dataOptions` for employee, department, subcompany, file, and related object fields. +- Request URL, method, content type, request parameters, request examples, response parameters, response examples, and error codes. +- At least two `curl` examples when documenting workflow creation: a minimal request and a full request. +- Workflow field tables grouped by business meaning, not by internal E10 UI group names. + +Never include in the external-facing document: +- Real E10 hostnames or IP addresses; use `https://`. +- Credentials, session cookies, real tokens, app secrets, or screenshots. +- E10 version numbers, internal document page IDs, internal API paths, database table names, `columnName`, or management UI instructions. +- Internal troubleshooting notes, browser-harness commands, XHR interception details, or generation timestamps. +- Informal warning labels such as “坑”, “注意”, “陷阱”, or “提示”; convert them into formal parameter notes or constraints. + +## Resource guide + +Use these bundled resources as needed: + +- `references/login-and-browser-harness.md`: isolated browser startup, login, captcha handling, session safety, cleanup. +- `references/openapi-doc-navigation.md`: how to navigate rendered E10 OpenAPI docs and distinguish similar API names. +- `references/workflow-field-discovery.md`: workflow lookup, form lookup, field extraction, main/detail table handling. +- `references/internal-apis.md`: read-only internal API chain for workflow/form/field metadata. +- `references/output-document-standard.md`: final documentation quality rules and field mapping conventions. +- `references/troubleshooting.md`: common failures and recovery steps. +- `assets/templates/e10-api-doc-template.md`: output skeleton for generated Markdown API documentation. +- `examples/`: sanitized examples showing prompt, extracted metadata, and expected output style. +- `evals/test-cases.md`: trigger and quality checks for maintaining this skill. + +## Completion checklist + +Before finishing, verify: +- The document is usable without access to E10 management pages. +- All URLs use `https://`. +- Authentication is complete and accurate. +- Request examples use external identifiers, such as job number and department code, rather than internal E10 IDs. +- No internal API path, database table name, session value, screenshot path, or version number appears in the final document. +- Field names, types, required flags, and examples are derived from actual documentation or explicit user-provided metadata. +- Missing or uncertain information is marked as “需确认” rather than fabricated. diff --git a/agents/openai.yaml b/agents/openai.yaml new file mode 100644 index 0000000..4379c67 --- /dev/null +++ b/agents/openai.yaml @@ -0,0 +1,13 @@ +interface: + display_name: Fanwei E10 API Doc + short_description: "Generate external-facing Markdown API docs for \u6CDB\u5FAE\ + \ OA E10 OpenAPI workflows." + icon: book-open + brand_color: '#2563EB' +policy: + products: + - chatgpt + - codex + - api + - atlas + allow_implicit_invocation: true diff --git a/assets/templates/e10-api-doc-template.md b/assets/templates/e10-api-doc-template.md new file mode 100644 index 0000000..9112d26 --- /dev/null +++ b/assets/templates/e10-api-doc-template.md @@ -0,0 +1,164 @@ +# <接口名称> + +## 接口说明 + +<一句话说明接口用途,面向外部系统开发人员。> + +## 前置条件 + +### 获取 access_token + +E10 OpenAPI 使用 OAuth2 code 换取 access_token 的方式。业务接口调用时,`access_token` 作为请求体参数传入,不通过 HTTP `Authorization` Header 传入。 + +#### Step 1: 获取 code + +- **URL**: `POST https:///papi/openapi/oauth2/authorize` +- **Content-Type**: `application/json` + +| 参数 | 必填 | 类型 | 说明 | +|---|---|---|---| +| `corpid` | 是 | String | 企业 corpId。 | +| `response_type` | 是 | String | 固定为 `code`。 | +| `state` | 是 | String | 自定义参数,建议使用随机字符串。 | + +请求示例: + +```json +{ + "corpid": "", + "response_type": "code", + "state": "state001" +} +``` + +响应示例: + +```json +{ + "errcode": "0", + "code": "" +} +``` + +#### Step 2: code 换 access_token + +- **URL**: `POST https:///papi/openapi/oauth2/access_token` +- **Content-Type**: `application/json` + +| 参数 | 必填 | 类型 | 说明 | +|---|---|---|---| +| `app_key` | 是 | String | 应用 key。 | +| `app_secret` | 是 | String | 应用密钥。 | +| `grant_type` | 是 | String | 固定为 `authorization_code`。 | +| `code` | 是 | String | 上一步获取的 code。 | + +请求示例: + +```json +{ + "app_key": "", + "app_secret": "", + "grant_type": "authorization_code", + "code": "" +} +``` + +响应示例: + +```json +{ + "errcode": "0", + "accessToken": "", + "refreshToken": "", + "expires_in": 7200 +} +``` + +## 身份标识转换 + +外部系统通常不持有 E10 内部用户 ID 或部门 ID。调用接口时应通过外部稳定标识进行转换。 + +| 标识类型 | 可选值 | 说明 | +|---|---|---| +| `userType` | `JOB_NUM` / `EMAIL` / `MOBILE` / `idNos` / `loginID` / `account` | 用于指定人员标识类型。推荐优先使用工号。 | +| `deptType` | `DEPT_CODE` / `DEPT_NAME` | 用于指定部门标识类型。推荐优先使用部门编号。 | + +表单中的人员、部门等关联字段应通过 `dataOptions` 传值,并指定相应的 `type`、`userType` 或 `deptType`。 + +## 请求信息 + +- **URL**: `POST https:///papi/openapi/` +- **Content-Type**: `application/json` + +## 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `access_token` | String | 是 | 接口调用凭证,请求体参数。 | +| `` | `` | `<是/否/需确认>` | `<说明>` | + +## 请求示例 + +### 最小请求 + +```bash +curl -X POST "https:///papi/openapi/" \ + -H "Content-Type: application/json" \ + -d '{ + "access_token": "" + }' +``` + +### 完整请求 + +```bash +curl -X POST "https:///papi/openapi/" \ + -H "Content-Type: application/json" \ + -d '{ + "access_token": "", + "userType": "JOB_NUM", + "deptType": "DEPT_CODE" + }' +``` + +## 响应参数 + +| 参数名 | 类型 | 说明 | +|---|---|---| +| `errcode` | String | 返回码,`0` 表示成功。 | +| `` | `` | `<说明>` | + +## 响应示例 + +### 成功 + +```json +{ + "errcode": "0", + "errmsg": "success" +} +``` + +### 失败 + +```json +{ + "errcode": "", + "errmsg": "" +} +``` + +## 错误码 + +| 错误码 | 说明 | +|---|---| +| `0` | 成功。 | +| `` | `<错误说明>` | + +## 流程字段说明 + +### <业务字段分组> + +| 字段名 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `<字段名>` | `<类型>` | `<是/否/需确认>` | `<说明>` | diff --git a/evals/test-cases.md b/evals/test-cases.md new file mode 100644 index 0000000..8154ca4 --- /dev/null +++ b/evals/test-cases.md @@ -0,0 +1,36 @@ +# fanwei-e10-api-doc Test Cases + +Use this file to test trigger behavior and output quality. + +## Should trigger + +1. 帮我写一份 E10 创建用车审批流程实例的接口文档。 +2. 根据泛微 E10 流程字段生成外部系统调用文档。 +3. 帮我整理 E10 OpenAPI 的 OAuth2 access_token 获取流程和请求示例。 +4. 写一个 E10 工作流程创建实例接口文档,输出 Markdown。 +5. 登录 E10 后帮我发现某个流程的表单字段,并生成接口说明。 + +## Should not trigger + +1. 帮我写一个普通 REST API 文档模板。 +2. 解释一下 OAuth2 是什么。 +3. 总结一下今天的会议纪要。 +4. 帮我写非 E10 系统的接口文档。 +5. 帮我修改 E10 表单字段配置。 + +## Output quality checks + +A generated external-facing document passes only if: + +1. All example URLs use `https://`. +2. No credentials, cookies, tokens, app secrets, real IPs, or hostnames appear. +3. No internal `/api/...` management endpoint appears. +4. No database table names or column names appear. +5. OAuth2 code-to-token flow is included. +6. The document states that `access_token` is a request-body parameter, not a Bearer header. +7. Identity mapping is included for `userType` and `deptType`. +8. Workflow field tables use `字段名 | 类型 | 必填 | 说明`. +9. Request examples use external identifiers such as job number and department code. +10. Missing requiredness or ambiguous mapping is marked `需确认`. +11. No detail-table section appears when the workflow has no detail table. +12. Browser-harness, XHR interception, and UI navigation details do not appear in the final document. diff --git a/examples/prompt-create-workflow-api-doc.md b/examples/prompt-create-workflow-api-doc.md new file mode 100644 index 0000000..efab397 --- /dev/null +++ b/examples/prompt-create-workflow-api-doc.md @@ -0,0 +1,10 @@ +# Example Prompt: Create Workflow API Documentation + +请帮我生成一份 E10 创建“用车审批流程”流程实例的接口文档。 + +背景: +- 读者是外部系统开发人员。 +- 文档格式为 Markdown。 +- 需要包含 OAuth2 access_token 获取流程。 +- 请求示例中不要使用 E10 内部用户 ID 或部门 ID,使用工号和部门编号。 +- 输出文件名:E10_用车审批流程_API.md diff --git a/examples/sample-field-extraction.json b/examples/sample-field-extraction.json new file mode 100644 index 0000000..d95f3f5 --- /dev/null +++ b/examples/sample-field-extraction.json @@ -0,0 +1,36 @@ +{ + "workflowName": "用车审批流程", + "workflowId": "", + "formId": "", + "mainFields": [ + { + "title": "申请人", + "dataKey": "applicant", + "type": "Employee", + "required": "需确认", + "businessGroup": "申请信息" + }, + { + "title": "申请部门", + "dataKey": "department", + "type": "Department", + "required": "需确认", + "businessGroup": "申请信息" + }, + { + "title": "用车日期", + "dataKey": "vehicleDate", + "type": "Date", + "required": "需确认", + "businessGroup": "用车信息" + }, + { + "title": "用车事由", + "dataKey": "reason", + "type": "TextArea", + "required": "需确认", + "businessGroup": "用车信息" + } + ], + "detailTables": [] +} diff --git a/examples/sample-output-api-doc.md b/examples/sample-output-api-doc.md new file mode 100644 index 0000000..8412e1d --- /dev/null +++ b/examples/sample-output-api-doc.md @@ -0,0 +1,87 @@ +# 创建用车审批流程实例接口 + +## 接口说明 + +该接口用于外部系统向 E10 发起用车审批流程实例。 + +## 前置条件 + +### 获取 access_token + +E10 OpenAPI 使用 OAuth2 code 换取 access_token 的方式。业务接口调用时,`access_token` 作为请求体参数传入,不通过 HTTP `Authorization` Header 传入。 + +#### Step 1: 获取 code + +- **URL**: `POST https:///papi/openapi/oauth2/authorize` +- **Content-Type**: `application/json` + +| 参数 | 必填 | 类型 | 说明 | +|---|---|---|---| +| `corpid` | 是 | String | 企业 corpId。 | +| `response_type` | 是 | String | 固定为 `code`。 | +| `state` | 是 | String | 自定义参数。 | + +#### Step 2: code 换 access_token + +- **URL**: `POST https:///papi/openapi/oauth2/access_token` +- **Content-Type**: `application/json` + +| 参数 | 必填 | 类型 | 说明 | +|---|---|---|---| +| `app_key` | 是 | String | 应用 key。 | +| `app_secret` | 是 | String | 应用密钥。 | +| `grant_type` | 是 | String | 固定为 `authorization_code`。 | +| `code` | 是 | String | 授权 code。 | + +## 身份标识转换 + +外部系统应使用工号和部门编号等外部稳定标识传值。人员字段使用 `userType: "JOB_NUM"`,部门字段使用 `deptType: "DEPT_CODE"`。 + +## 请求信息 + +- **URL**: `POST https:///papi/openapi/` +- **Content-Type**: `application/json` + +## 请求示例 + +### 最小请求 + +```bash +curl -X POST "https:///papi/openapi/" \ + -H "Content-Type: application/json" \ + -d '{ + "access_token": "", + "userType": "JOB_NUM", + "deptType": "DEPT_CODE", + "formData": { + "dataDetails": [ + { + "dataKey": "applicant", + "dataOptions": [ + {"type": "resource", "value": "EMP001", "userType": "JOB_NUM"} + ] + }, + { + "dataKey": "vehicleDate", + "content": "2026-06-01" + } + ] + } + }' +``` + +## 流程字段说明 + +### 申请信息 + +| 字段名 | 类型 | 必填 | 说明 | +|---|---|---|---| +| 申请人 | Employee | 需确认 | 人员字段,通过 `dataOptions` 传值,建议使用工号。 | +| 申请部门 | Department | 需确认 | 部门字段,通过 `dataOptions` 传值,建议使用部门编号。 | + +### 用车信息 + +| 字段名 | 类型 | 必填 | 说明 | +|---|---|---|---| +| 用车日期 | Date | 需确认 | 日期字符串。 | +| 用车事由 | TextArea | 需确认 | 用车原因说明。 | diff --git a/references/internal-apis.md b/references/internal-apis.md index 7a58031..c19e001 100644 --- a/references/internal-apis.md +++ b/references/internal-apis.md @@ -1,257 +1,175 @@ -# E10 内部 API 参考 +# E10 Internal Read-Only APIs -这些是 E10 管理后台前端调用的内部 REST API(非 OpenAPI),在浏览器已登录的情况下可直接用 `fetch()` 调用,session cookie 自动携带。适合替代复杂的 UI 点击操作获取数据。 +These APIs are E10 management UI backend APIs, not external OpenAPI endpoints. Use them only inside an authenticated browser session to read metadata needed for documentation. Do not expose these paths, request bodies, response fields, database table names, or internal IDs in the final external-facing documentation. -> **适用场景**:登录后(Phase 1),所有流程发现、表单查询、字段提取均可通过这些 API 完成,无需逐层点击 UI。 +## Use cases ---- +Use these APIs after login to avoid fragile UI navigation when discovering workflow fields: -## 1. 获取所有工作流列表 +1. Get workflow list and target `workflowId`. +2. Search form list and target `formId`. +3. Read form metadata and detect detail tables. +4. Read main-table and detail-table field definitions. -**用途**:按分类树形列出所有工作流,找到目标流程的 `workflowId`(也叫 pathSetId)。 +All examples use `fetch()` inside the browser session, so the authenticated session cookie is included automatically. -``` +## 1. Get workflow list + +Endpoint: + +```text POST /api/bs/workflow/pathdef/baseSet/getBaseInfoListTree ``` -**请求体**: +Request body: ```json { - "searchParams": { - "otherSearchDatas": { - "workflowType": "", - "workflowId": "", - "subCompanyId": "" - } - }, - "isTemplate": 0, - "belongType": 1 -} -``` - -**响应结构**: - -```json -{ - "code": 200, - "data": { - "total": 42, - "dataGroupTitle": [ - {"typeName": "行政类", "count": 9, "id": "1184726218683441153"} - ], - "datas": [ - { - "typeName": "行政类", - "children": [ - { - "id": "1185512781721886720", - "title": "06.用车审批流程", - "defaultWorkflowName": "06.用车审批流程", - "status": "1" - } - ] - } - ] + "searchParams": { + "otherSearchDatas": { + "workflowType": "", + "workflowId": "", + "subCompanyId": "" } + }, + "isTemplate": 0, + "belongType": 1 } ``` -**关键字段**: -- `datas[].children[].id` — workflowId(pathSetId),用于后续查询 -- `datas[].children[].title` — 流程名称 -- `datas[].typeName` — 流程分类(人事类/行政类/财务类/业务类) +Important response fields: ---- +- `data.datas[].typeName`: workflow category. +- `data.datas[].children[].id`: workflowId/pathSetId. +- `data.datas[].children[].title`: workflow display name. -## 2. 搜索表单(按名称) - -**用途**:根据表单名称搜索,获取 `formId`。名称支持模糊匹配。 +Browser snippet: +```python +import json +result = json.loads(js(''' +(async () => { + let resp = await fetch("/api/bs/workflow/pathdef/baseSet/getBaseInfoListTree", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + searchParams: { otherSearchDatas: { workflowType: "", workflowId: "", subCompanyId: "" } }, + isTemplate: 0, + belongType: 1 + }) + }); + return JSON.stringify(await resp.json()); +})(); +''')) ``` + +## 2. Search forms + +Endpoint: + +```text POST /api/workflow/core/form/formmanage/getFormList ``` -**请求体**: +Request body: ```json { - "module": "workflow", - "pageNo": 1, - "pageSize": 50, - "name": "用车" + "module": "workflow", + "pageNo": 1, + "pageSize": 50, + "name": "" } ``` -**响应结构**: +Important response fields: -```json -{ - "code": 200, - "data": { - "pageDatas": { - "result": [ - { - "id": "1185512764483297325", - "name": "用车审批流程" - } - ] - } - } -} -``` +- `data.pageDatas.result[].id`: formId. +- `data.pageDatas.result[].name`: form name. -**关键字段**: -- `data.pageDatas.result[].id` — formId -- `data.pageDatas.result[].name` — 表单名称 +Workflow names may include numbering or prefixes, while form names often do not. Search by core business keyword rather than exact full workflow title. -> **注意**:流程名称(如"06.用车审批流程")与表单名称(如"用车审批流程")可能不同(有无编号前缀)。搜索时去掉编号前缀。 +## 3. Get form metadata ---- +Endpoint: -## 3. 获取表单详情 - -**用途**:获取表单的元数据(表名、类型、基础设置)。 - -``` +```text POST /api/workflow/core/form/formmanage/getForm ``` -**请求体**: +Request body: ```json { - "module": "workflow", - "form": { - "id": "1185512764483297325" - } + "module": "workflow", + "form": { + "id": "" + } } ``` -**响应结构**: +Important response fields: -```json -{ - "code": 200, - "data": { - "id": "1185512764483297325", - "tableName": "ft_ycsplc", - "name": "用车审批流程", - "tableType": "MAIN", - "systemTable": false, - "editTable": true - } -} -``` +- `data.name`: form name. +- `data.tableType`: form table type. +- `data.detailTable`: detail table metadata when available. +- `data.tableName`: internal database table name. Use only for internal reasoning and never include it in final external documentation. -**关键字段**: -- `data.tableName` — 数据库表名 -- `data.tableType` — MAIN(主表)或 DETAIL(明细表) -- `data.name` — 表单名称 +## 4. Get field definitions ---- +Endpoint: -## 4. 获取表单字段列表(分页) - -**用途**:获取主表或明细表的所有字段定义(含 fieldId、dataKey、类型、分组)。 - -``` +```text POST /api/workflow/core/form/field/manage/getFormFieldPage ``` -**请求体(主表字段)**: +Request body: ```json { - "pageNo": 1, - "pageSize": 20, - "module": "workflow", - "formFieldSearchEntity": { - "isDelete": 0, - "status": "enable", - "formId": "1185512764483297325", - "formTableId": "1185512764483297435" - } + "pageNo": 1, + "pageSize": 100, + "module": "workflow", + "formFieldSearchEntity": { + "isDelete": 0, + "status": "enable", + "formId": "" + } } ``` -**请求体(明细表字段)**:同上,`formTableId` 替换为明细表的 table ID。 +If needed, add `formTableId` to target a specific main or detail table. If passing `formTableId` returns zero records for the main table, retry without `formTableId`. -**响应结构**: +Important response fields: -```json -{ - "code": 200, - "data": { - "pageDatas": { - "pageNo": 1, - "pageSize": 20, - "result": [ - { - "title": "申请人", - "dataKey": "sqr", - "type": "Employee", - "fieldId": "1185512764483297398", - "showOrder": 0, - "groupName": "基础信息", - "columnName": "sqr", - "formTableId": "1185512764483297435", - "formId": "1185512764483297325" - } - ] - } - } -} -``` +- `title`: business field display name. +- `dataKey`: request payload field key. +- `fieldId`: internal field ID. Usually not exposed unless the OpenAPI payload explicitly requires it. +- `type`: E10 component/API field type. +- `showOrder`: display order. +- `groupName`: internal UI group. Use only to infer business grouping; do not copy it blindly. +- `columnName`: internal database column. Never include in external documentation. +- `formTableId`: table ID, useful for follow-up field queries. -**关键字段**: -- `title` — 字段显示名称 -- `dataKey` — 字段编码(API 传参用) -- `fieldId` — 字段 ID(API 传参用,纯数字) -- `type` — 字段类型(Employee/Date/Text/TextArea/Department/SubCompany/File/EBuilder/Flow/Document) -- `showOrder` — 显示顺序 -- `groupName` — 所属分组 -- `columnName` — 数据库列名 +## Type mapping for external documentation -**常见字段类型映射**: +| E10 type | External description | Common payload guidance | +|---|---|---| +| Employee | Personnel field | Use `dataOptions`, `type: "resource"`, and `userType` such as `JOB_NUM`, `EMAIL`, `MOBILE`, `idNos`, `loginID`, or `account`. | +| Department | Department field | Use `dataOptions`, `type: "department"`, and `deptType` such as `DEPT_CODE` or `DEPT_NAME`. | +| SubCompany | Sub-company field | Use `dataOptions`, `type: "subcompany"`; prefer an external organization code when supported. | +| Date | Date | Use ISO-like date string such as `2026-05-27`. | +| Text | Single-line text | Use `content` string. | +| TextArea | Multi-line text | Use `content` string. | +| File | Attachment | Upload file first, then reference upload result in `dataOptions`. | +| EBuilder | Related e-builder object | Use documented option/object mapping. Mark as needing confirmation if not clear. | +| Flow | Related workflow | Use documented option/object mapping. Mark as needing confirmation if not clear. | +| Document | Related document | Use documented option/object mapping. Mark as needing confirmation if not clear. | -| API type | 中文含义 | formData 传值方式 | -|----------|----------|-------------------| -| Employee | 人员选择 | `dataOptions[].type: "resource"` | -| Department | 部门选择 | `dataOptions[].type: "department"` | -| SubCompany | 分部选择 | `dataOptions[].type: "subcompany"` | -| Date | 日期 | `content: "2026-05-27"` | -| Text | 单行文本 | `content: "文本值"` | -| TextArea | 多行文本 | `content: "文本值"` | -| File | 附件 | `dataOptions[].type: "file"` + uploadParam | -| EBuilder | 关联e-builder | 选项型传值 | -| Flow | 关联流程 | 选项型传值 | -| Document | 关联文档 | 选项型传值 | +## Alternative formId discovery ---- +If `getFormList` cannot identify the form, use these fallback methods: -## 5. 获取 formTableId(主表和明细表) - -formTableId 可以从以下途径获取: - -1. **XHR 拦截**:在字段管理页面切换 主表/明细表 标签时,页面会调用 `getFormFieldPage`,其请求体中的 `formTableId` 即为当前表的 ID。 -2. **字段响应反推**:主表任一字段的 `formTableId` 即为主表的 table ID。 -3. **明细表为空时**:不需要 formTableId。 - ---- - -## formId 的其他获取方式 - -除了 `getFormList` API 搜索,还可从以下途径获取 formId: - -1. **localStorage**:访问过表单管理页面后,`localStorage` 中会有 key `Form_0__checkNewForm`,从中提取数字部分。 - - ```js - for (var key in localStorage) { - if (key.startsWith("Form_0_") && key.endsWith("_checkNewForm")) { - return key.replace("Form_0_", "").replace("_checkNewForm", ""); - } - } - ``` - -2. **URL 路径**(不可靠,仅作参考):流程详情页 URL 中的 `pathset/` 后数字是 workflowId,不是 formId。 +1. After visiting form management UI, inspect localStorage keys matching `Form_0__checkNewForm`. +2. Intercept XHR requests to `getFormFieldPage` after switching field tabs and inspect the request body. +3. Do not treat `pathset/` in the workflow detail URL as formId; that value is workflowId/pathSetId. diff --git a/references/login-and-browser-harness.md b/references/login-and-browser-harness.md new file mode 100644 index 0000000..b61fcac --- /dev/null +++ b/references/login-and-browser-harness.md @@ -0,0 +1,144 @@ +# Login and Browser Harness + +Use this reference only when live E10 access is required. + +## Safety requirements + +- Use an isolated Chrome instance, never the user's daily browser profile. +- Do not store credentials in files, memory, shell history notes, examples, or final documentation. +- Keep all E10 activity read-only. +- Clean up the isolated browser process when finished. + +## Cross-platform placeholders + +| Placeholder | Linux | macOS | Windows | +|---|---|---|---| +| `` | `google-chrome` | `/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome` | `"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"` | +| `` | `/tmp/bh-e10` | `/tmp/bh-e10` | `%TEMP%\\bh-e10` | +| `` | `/tmp` | `/tmp` | `%TEMP%` | + +## Start isolated Chrome + +Clean stale browser-harness daemons first. Then start Chrome with a dedicated remote debugging port and profile directory. + +Linux/macOS pattern: + +```bash +pkill -f browser_harness.daemon 2>/dev/null; rm -f /tmp/bu-*.sock /tmp/bu-*.pid 2>/dev/null + --headless=new --remote-debugging-port=9223 --user-data-dir= --no-first-run --no-default-browser-check --window-size=1920,1080 +sleep 3 && curl -s http://127.0.0.1:9223/json/version | grep webSocketDebuggerUrl +``` + +Every browser-harness command must set `BU_CDP_URL` in the same shell command: + +```bash +export BU_CDP_URL=http://127.0.0.1:9223 && browser-harness <<'PY' +# commands +PY +``` + +Do not run `export BU_CDP_URL=...` on a separate line and then invoke `browser-harness`; the daemon may not inherit the later shell environment. + +Windows pattern: + +```bat +set BU_CDP_URL=http://127.0.0.1:9223 && browser-harness +``` + +## Navigate and detect session state + +```python +new_tab("https:///login") +wait_for_load() +wait(3) +capture_screenshot("/e10_login.png") +print("URL:", js("window.location.href")) +``` + +If the URL is not `/login`, an existing session may already be authenticated in the isolated profile. Continue only if the current page confirms a valid E10 session. + +## Captcha handling + +E10 captcha is expected to be a four-digit number. If visual recognition returns fewer than four digits, refresh the captcha image and capture again. + +```python +capture_screenshot("/e10_captcha.png") +``` + +Use available visual inspection capability to read the four digits. If incomplete, click the captcha image center and retry. + +## Fill login form reliably + +E10 input fields may not respond to direct `element.value = ...`. Use the native input setter and dispatch events. + +```python +import json + +info = json.loads(js(''' +var inputs = document.querySelectorAll("input"); +var result = []; +for (var i = 0; i < inputs.length; i++) { + var r = inputs[i].getBoundingClientRect(); + result.push({ + dataId: inputs[i].getAttribute("data-id"), + x: Math.round(r.left + r.width/2), + y: Math.round(r.top + r.height/2) + }); +} +var btn = document.querySelector("button"); +var br = btn ? btn.getBoundingClientRect() : null; +return JSON.stringify({inputs: result, btn: br ? {x: Math.round(br.left+br.width/2), y: Math.round(br.top+br.height/2)} : null}); +''')) + +js(''' +var ns = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; +window.__fi = function(dataId, value) { + var el = document.querySelector("[data-id='" + dataId + "']"); + el.focus(); + el.value = ""; + ns.call(el, value); + el.dispatchEvent(new Event("input", { bubbles: true })); + el.dispatchEvent(new Event("change", { bubbles: true })); + el.dispatchEvent(new Event("blur", { bubbles: true })); +}; +''') + +u = info["inputs"][0] +p = info["inputs"][1] +c = info["inputs"][2] +b = info["btn"] + +click_at_xy(u["x"], u["y"]); wait(0.2) +js('window.__fi("' + u["dataId"] + '", "")') +click_at_xy(p["x"], p["y"]); wait(0.2) +js('window.__fi("' + p["dataId"] + '", "")') +click_at_xy(c["x"], c["y"]); wait(0.2) +js('window.__fi("' + c["dataId"] + '", "")') + +print(js(''' +var vals = []; +document.querySelectorAll("input").forEach(function(el){ vals.push(el.value ? "set" : "empty"); }); +JSON.stringify(vals); +''')) + +click_at_xy(b["x"], b["y"]) +wait(5) +wait_for_load() +print("After login:", js("window.location.href")) +``` + +If still on `/login`, assume captcha or credentials failed. Refresh captcha and retry rather than proceeding. + +## Cleanup + +Linux/macOS: + +```bash +pkill -f "chrome.*9223" 2>/dev/null +``` + +Windows: + +```bat +taskkill /F /IM chrome.exe 2>nul +``` diff --git a/references/openapi-doc-navigation.md b/references/openapi-doc-navigation.md new file mode 100644 index 0000000..0346e1d --- /dev/null +++ b/references/openapi-doc-navigation.md @@ -0,0 +1,155 @@ +# OpenAPI Documentation Navigation + +Use this reference to inspect E10 OpenAPI documentation pages. + +## Documentation homepage + +Navigate to: + +```text +https:///sp/openapi/base/doc/index +``` + +E10 documentation pages are React SPA pages. Plain `curl` usually retrieves only a shell page. Use a rendered browser session and wait for JavaScript content. + +```python +new_tab("https:///sp/openapi/base/doc/index") +wait(8) +wait_for_load() +wait(3) +capture_screenshot("/tmp/e10_api_doc_home.png") +``` + +## Extract category links + +The documentation homepage exposes categories through `` tags. Extract link text and href values, then navigate directly to the target href instead of clicking. + +```python +import json +links = json.loads(js(''' +var all = document.querySelectorAll("a"); +var result = []; +for (var i = 0; i < all.length; i++) { + var a = all[i]; + var txt = a.textContent.trim(); + if (txt.length > 0 && txt.length < 50 && a.href) { + result.push({text: txt, href: a.href}); + } +} +return JSON.stringify(result); +''')) +for l in links: + print(l["text"], "->", l["href"]) +``` + +**Pitfall**: The href values extracted from the doc index page (e.g. `https:///sp/opendoc/freepass/.../zh_cn/840714220321120257`) may return 404 when navigated to directly. These URLs appear to be for a separate doc viewer that is not directly accessible from the OpenAPI docs SPA. Instead, always navigate through the SPA sidebar — first load the auth/page page (`/sp/opendoc/freepass/.../zh_cn/840673181800431617` which is the "认证" page), then use sidebar JS clicks to reach other API docs. Do NOT attempt to construct doc page URLs from the index page links. + +## Read rendered page text + +After navigating to a category or API page, extract `document.body.innerText`. Long pages may need scrolling and repeated extraction. + +```python +content = js("document.body.innerText") +print(content[:15000]) +``` + +If content appears incomplete: + +```python +js("window.scrollBy(0, 800)") +wait(2) +print(js("document.body.innerText")[8000:15000]) +``` + +## Sidebar navigation (SPA — JS click required) + +The documentation page is a React SPA. Sidebar tree items use class `.ui-tree-bar` and do not respond reliably to ref-based `browser_click`. Use JavaScript to expand categories and navigate to sub-items. + +### Expand a category + +```python +# Expand "工作流程" category +js(''' +(function() { + var items = document.querySelectorAll(".ui-tree-bar"); + for (var i = 0; i < items.length; i++) { + if (items[i].textContent.trim() === "工作流程") { + items[i].click(); + return "expanded"; + } + } + return "not found"; +})(); +''') +``` + +### Navigate to a sub-item + +After expanding the parent category, navigate by clicking on the sub-item text element (class `.ui-tree-node`): + +```python +# Click "创建流程实例" sub-item +js(''' +(function() { + var items = document.querySelectorAll(".ui-tree-bar"); + for (var i = 0; i < items.length; i++) { + if (items[i].textContent.trim() === "创建流程实例") { + items[i].click(); + return "clicked"; + } + // Also try clicking the .ui-tree-node child + var node = items[i].querySelector(".ui-tree-node"); + if (node && node.textContent.trim() === "创建流程实例") { + node.click(); + return "clicked node"; + } + } + return "not found"; +})(); +''') +``` + +The sub-items appear as `
  • ` elements with class `.ui-tree-bar` at `level=2` inside the parent category's `