diff --git a/hub/package-lock.json b/hub/package-lock.json index e8fb606..748b2ae 100644 --- a/hub/package-lock.json +++ b/hub/package-lock.json @@ -8,6 +8,9 @@ "name": "@paradigm/hub", "version": "0.0.31", "dependencies": { + "@alicloud/credentials": "^2.4.5", + "@alicloud/docmind-api20220711": "^1.4.15", + "@alicloud/tea-util": "^1.4.11", "@anthropic-ai/claude-agent-sdk": "^0.3.202", "@fastify/cookie": "^11.0.2", "@larksuiteoapi/node-sdk": "^1.70.0", @@ -74,6 +77,175 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@alicloud/credentials": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@alicloud/credentials/-/credentials-2.4.5.tgz", + "integrity": "sha512-od1ufCxOO7cP2R4EVFliOB0kGo9lUXCibyj/mzmI6yLhxeqhqsegTzVsx5p2NJJsceKJnYcmye7FWKyLJAFBkw==", + "license": "MIT", + "dependencies": { + "@alicloud/tea-typescript": "^1.8.0", + "httpx": "^2.3.3", + "ini": "^1.3.5", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/darabonba-array": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-array/-/darabonba-array-0.1.2.tgz", + "integrity": "sha512-ZPuQ+bJyjrd8XVVm55kl+ypk7OQoi1ZH/DiToaAEQaGvgEjrTcvQkg71//vUX/6cvbLIF5piQDvhrLb+lUEIPQ==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/darabonba-encode-util": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.2.tgz", + "integrity": "sha512-mlsNctkeqmR0RtgE1Rngyeadi5snLOAHBCWEtYf68d7tyKskosXDTNeZ6VCD/UfrUu4N51ItO8zlpfXiOgeg3A==", + "license": "ISC", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/@alicloud/darabonba-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-map/-/darabonba-map-0.0.1.tgz", + "integrity": "sha512-2ep+G3YDvuI+dRYVlmER1LVUQDhf9kEItmVB/bbEu1pgKzelcocCwAc79XZQjTcQGFgjDycf3vH87WLDGLFMlw==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/darabonba-signature-util": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-signature-util/-/darabonba-signature-util-0.0.4.tgz", + "integrity": "sha512-I1TtwtAnzLamgqnAaOkN0IGjwkiti//0a7/auyVThdqiC/3kyafSAn6znysWOmzub4mrzac2WiqblZKFcN5NWg==", + "license": "ISC", + "dependencies": { + "@alicloud/darabonba-encode-util": "^0.0.1" + } + }, + "node_modules/@alicloud/darabonba-signature-util/node_modules/@alicloud/darabonba-encode-util": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.1.tgz", + "integrity": "sha512-Sl5vCRVAYMqwmvXpJLM9hYoCHOMsQlGxaWSGhGWulpKk/NaUBArtoO1B0yHruJf1C5uHhEJIaylYcM48icFHgw==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1", + "moment": "^2.29.1" + } + }, + "node_modules/@alicloud/darabonba-string": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-string/-/darabonba-string-1.0.3.tgz", + "integrity": "sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1" + } + }, + "node_modules/@alicloud/docmind-api20220711": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@alicloud/docmind-api20220711/-/docmind-api20220711-1.4.15.tgz", + "integrity": "sha512-QmjSDPV52d2B5bd/Dk3Zeofu+cJFPKYS+MphIHqlulfYsOynLgOaDqTRMGTK/RhcmVCwG14CyZ/15GBF00GRFw==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/openapi-core": "^1.0.0", + "@darabonba/typescript": "^1.0.0" + } + }, + "node_modules/@alicloud/endpoint-util": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", + "integrity": "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/gateway-pop": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@alicloud/gateway-pop/-/gateway-pop-0.0.6.tgz", + "integrity": "sha512-KF4I+JvfYuLKc3fWeWYIZ7lOVJ9jRW0sQXdXidZn1DKZ978ncfGf7i0LBfONGk4OxvNb/HD3/0yYhkgZgPbKtA==", + "license": "ISC", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/darabonba-array": "^0.1.0", + "@alicloud/darabonba-encode-util": "^0.0.2", + "@alicloud/darabonba-map": "^0.0.1", + "@alicloud/darabonba-signature-util": "^0.0.4", + "@alicloud/darabonba-string": "^1.0.2", + "@alicloud/endpoint-util": "^0.0.1", + "@alicloud/gateway-spi": "^0.0.8", + "@alicloud/openapi-util": "^0.3.2", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.4.8" + } + }, + "node_modules/@alicloud/gateway-spi": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz", + "integrity": "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==", + "license": "ISC", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/openapi-core": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-core/-/openapi-core-1.0.8.tgz", + "integrity": "sha512-xs8LdgMDcEUqv13kZ4nl+Vd+Fc1mixTF0g1lm5QSrNWDaLUUD5vqpuMtvUZnKNnq9PITfUQ+8KunAvNQJpFo8g==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/gateway-pop": "0.0.6", + "@alicloud/gateway-spi": "^0.0.8", + "@darabonba/typescript": "^1.0.5" + } + }, + "node_modules/@alicloud/openapi-util": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-util/-/openapi-util-0.3.3.tgz", + "integrity": "sha512-vf0cQ/q8R2U7ZO88X5hDiu1yV3t/WexRj+YycWxRutkH/xVXfkmpRgps8lmNEk7Ar+0xnY8+daN2T+2OyB9F4A==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.3.0", + "kitx": "^2.1.0", + "sm3": "^1.0.3" + } + }, + "node_modules/@alicloud/tea-typescript": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz", + "integrity": "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==", + "license": "ISC", + "dependencies": { + "@types/node": "^12.0.2", + "httpx": "^2.2.6" + } + }, + "node_modules/@alicloud/tea-typescript/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/@alicloud/tea-util": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@alicloud/tea-util/-/tea-util-1.4.11.tgz", + "integrity": "sha512-HyPEEQ8F0WoZegiCp7sVdrdm6eBOB+GCvGl4182u69LDFktxfirGLcAx3WExUr1zFWkq2OSmBroTwKQ4w/+Yww==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "@darabonba/typescript": "^1.0.0", + "kitx": "^2.0.0" + } + }, "node_modules/@anthropic-ai/claude-agent-sdk": { "version": "0.3.202", "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.202.tgz", @@ -234,6 +406,24 @@ "node": ">=6.9.0" } }, + "node_modules/@darabonba/typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@darabonba/typescript/-/typescript-1.0.5.tgz", + "integrity": "sha512-pfxHFVM8I3h8K2o8skpDQLMmR5iAeQ2eNpP1HrKDEm/9ZPF8aKwDKPwwEszT1NnIo0Y3++E7x1YsVkVpGjA/xw==", + "license": "Apache License 2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "httpx": "^2.3.2", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "moment-timezone": "^0.5.45", + "socks-proxy-agent": "^6.2.1", + "ws": "^8.18.0", + "xml2js": "^0.6.2" + } + }, "node_modules/@emnapi/core": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", @@ -1396,6 +1586,15 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -2830,6 +3029,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -2843,6 +3056,25 @@ "node": ">= 6" } }, + "node_modules/httpx": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/httpx/-/httpx-2.3.3.tgz", + "integrity": "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==", + "license": "MIT", + "dependencies": { + "@types/node": "^20", + "debug": "^4.1.1" + } + }, + "node_modules/httpx/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/iconv-lite": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", @@ -2867,12 +3099,17 @@ "license": "ISC", "peer": true }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 12" } @@ -2972,6 +3209,15 @@ "license": "BSD-2-Clause", "peer": true }, + "node_modules/kitx": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/kitx/-/kitx-2.2.0.tgz", + "integrity": "sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg==", + "license": "MIT", + "dependencies": { + "@types/node": "^22.5.4" + } + }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", @@ -3270,6 +3516,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.identity": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-3.0.0.tgz", @@ -3357,6 +3609,27 @@ "node": ">= 0.6" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3976,6 +4249,15 @@ "license": "MIT", "peer": true }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -4193,6 +4475,50 @@ "dev": true, "license": "ISC" }, + "node_modules/sm3": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sm3/-/sm3-1.0.3.tgz", + "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==", + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -4700,6 +5026,28 @@ } } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/hub/package.json b/hub/package.json index 9e32332..3cbdf44 100644 --- a/hub/package.json +++ b/hub/package.json @@ -7,6 +7,9 @@ "node": ">=24" }, "dependencies": { + "@alicloud/credentials": "^2.4.5", + "@alicloud/docmind-api20220711": "^1.4.15", + "@alicloud/tea-util": "^1.4.11", "@anthropic-ai/claude-agent-sdk": "^0.3.202", "@fastify/cookie": "^11.0.2", "@larksuiteoapi/node-sdk": "^1.70.0", diff --git a/hub/src/capability/capabilityConnections.ts b/hub/src/capability/capabilityConnections.ts index c785435..ce02de6 100644 --- a/hub/src/capability/capabilityConnections.ts +++ b/hub/src/capability/capabilityConnections.ts @@ -63,8 +63,8 @@ export async function resolveCapabilityCredential( organizationId: connection.organizationId, capabilityId: connection.capabilityId, schemaVersion: 1, - baseUrl: payload.baseUrl, - apiToken: payload.apiToken, - projectId: payload.projectId, + accessKeyId: payload.accessKeyId, + accessKeySecret: payload.accessKeySecret, + endpoint: payload.endpoint, }; } diff --git a/hub/src/capability/docmindClient.ts b/hub/src/capability/docmindClient.ts new file mode 100644 index 0000000..68c3533 --- /dev/null +++ b/hub/src/capability/docmindClient.ts @@ -0,0 +1,194 @@ +/** + * ADR-0027: Alibaba Cloud Document Mind (docmind) client. + * + * Uses the official @alicloud/docmind-api20220711 SDK to call the document + * parsing (large model version) API. The API is asynchronous: + * 1. SubmitDocParserJobAdvance — upload local file, get job id + * 2. QueryDocParserStatus — poll until completed + * 3. GetDocParserResult — fetch markdown + page images + * + * Pricing (2025-07, aliyun docmind): + * - 图文文档基础链路: 0.02元/页 + * - 图文文档增强链路 (含公式 LaTeX): 0.04元/页 + * - 视频: 0.002元/秒 + * - 音频: 0.00035元/秒 + * + * The adapter requests OutputFormat=markdown and FormulaEnhancement=true for + * PDF inputs. Page images are returned as URLs and downloaded into the + * workspace by the adapter. + */ +import $DocmindClient, { + SubmitDocParserJobAdvanceRequest, + QueryDocParserStatusRequest, + GetDocParserResultRequest, +} from "@alicloud/docmind-api20220711"; +import { RuntimeOptions } from "@alicloud/tea-util"; +import { readFile } from "node:fs/promises"; +import { basename } from "node:path"; +import type { CapabilitySecretPayload } from "./types.js"; + +/** A single extracted image (page render) from the parsed document. */ +export interface DocmindExtractedImage { + /** Suggested relative filename (e.g. "page_1.jpg"). */ + readonly filename: string; + /** Raw image bytes (downloaded from the docmind URL). */ + readonly data: Uint8Array; +} + +/** The structured result of parsing one document. */ +export interface DocmindParseResult { + /** Markdown text with image references (relative to output dir). */ + readonly markdown: string; + /** Page images extracted from the document, downloaded as bytes. */ + readonly images: readonly DocmindExtractedImage[]; + /** Number of pages processed (for UsageFact quantity, unit "pages"). */ + readonly pageCount: number; + /** USD cost if the service reported one; null if unknown (ADR-0022). + * docmind bills in CNY per page; cost is derived from page count × unit price. */ + readonly costUsd: number | null; + /** External job id for the UsageFact correlationId. */ + readonly requestId: string | null; +} + +/** Options passed to the client. */ +export interface DocmindParseOptions { + /** Absolute path to the input file on the Hub's filesystem. */ + readonly inputFilePath: string; +} + +/** + * Client interface for the Alibaba Cloud Document Mind parsing service. + * The real implementation uses the official SDK; tests inject a mock. + */ +export interface CapabilityProviderClient { + parse(credential: CapabilitySecretPayload, options: DocmindParseOptions): Promise; +} + +/** Errors raised by the docmind client. */ +export class DocmindClientError extends Error { + constructor( + message: string, + readonly code: "docmind_unreachable" | "docmind_rejected" | "docmind_invalid_response" | "docmind_no_output" | "docmind_timeout", + readonly upstreamStatus?: number, + ) { + super(message); + this.name = "DocmindClientError"; + } +} + +/** Cost per page in USD (0.04 CNY/page ≈ 0.0056 USD, rate ~7.15). Updated when pricing confirmed. */ +const COST_PER_PAGE_USD = 0.0056; + +/** Poll interval for QueryDocParserStatus (ms). Aliyun recommends 10s. */ +const POLL_INTERVAL_MS = 10_000; +/** Max poll duration (ms). Aliyun allows 120 minutes; we cap at 5 minutes for a single page bundle. */ +const POLL_TIMEOUT_MS = 5 * 60_000; + +/** Type alias for the SDK client constructor's config parameter. */ +type DocmindConfig = ConstructorParameters[0]; + +/** + * Real Alibaba Cloud Document Mind client using the official SDK. + * Submits a local file, polls for completion, fetches the markdown result, + * and downloads page images. + */ +export class AliyunDocmindClient implements CapabilityProviderClient { + async parse(credential: CapabilitySecretPayload, options: DocmindParseOptions): Promise { + const config: DocmindConfig = { + endpoint: credential.endpoint, + accessKeyId: credential.accessKeyId, + accessKeySecret: credential.accessKeySecret, + type: "access_key", + regionId: "cn-hangzhou", + } as DocmindConfig; + const client = new $DocmindClient.default(config); + + // 1. Submit job with local file upload. + const fileName = basename(options.inputFilePath); + const fileBuffer = await readFile(options.inputFilePath); + const advanceRequest = new SubmitDocParserJobAdvanceRequest({ + fileUrlObject: fileBuffer, + fileName, + outputFormat: ["markdown"], + formulaEnhancement: true, + }); + const runtime = new RuntimeOptions({}); + let submitResponse; + try { + submitResponse = await client.submitDocParserJobAdvance(advanceRequest, runtime); + } catch (e) { + throw new DocmindClientError( + e instanceof Error ? e.message : String(e), + "docmind_unreachable", + ); + } + const jobId = submitResponse.body?.data?.id; + if (jobId === undefined || jobId === null || jobId === "") { + throw new DocmindClientError("docmind submit returned no job id", "docmind_invalid_response"); + } + + // 2. Poll until completed. + const deadline = Date.now() + POLL_TIMEOUT_MS; + let completed = false; + let status = ""; + while (Date.now() < deadline) { + await sleep(POLL_INTERVAL_MS); + const statusReq = new QueryDocParserStatusRequest({ id: jobId }); + const statusResponse = await client.queryDocParserStatus(statusReq); + const body = statusResponse.body as { completed?: boolean; status?: string }; + completed = body.completed === true; + status = body.status ?? ""; + if (completed) break; + } + if (!completed) { + throw new DocmindClientError(`docmind job ${jobId} timed out (status: ${status})`, "docmind_timeout"); + } + if (status === "Fail") { + throw new DocmindClientError(`docmind job ${jobId} failed`, "docmind_rejected"); + } + + // 3. Fetch result. + const resultReq = new GetDocParserResultRequest({ id: jobId }); + const resultResponse = await client.getDocParserResult(resultReq); + const resultBody = resultResponse.body as { + data?: { markdown?: string; docInfo?: { pages?: Array<{ imageUrl?: string; pageIdCurDoc?: number }> } }; + }; + const markdown = resultBody.data?.markdown ?? ""; + if (markdown === "") { + throw new DocmindClientError("docmind returned empty markdown", "docmind_no_output"); + } + + // 4. Download page images. + const pages = resultBody.data?.docInfo?.pages ?? []; + const images: DocmindExtractedImage[] = []; + for (const page of pages) { + if (page.imageUrl === undefined || page.imageUrl === null || page.imageUrl === "") continue; + try { + const resp = await fetch(page.imageUrl); + if (!resp.ok) continue; + const data = new Uint8Array(await resp.arrayBuffer()); + const pageNum = page.pageIdCurDoc ?? images.length + 1; + images.push({ filename: `page_${pageNum}.jpg`, data }); + } catch { + // Best-effort: skip images that fail to download. + } + } + + const pageCount = pages.length > 0 ? pages.length : countMarkdownPages(markdown); + const costUsd = pageCount * COST_PER_PAGE_USD; + + return { markdown, images, pageCount, costUsd, requestId: jobId }; + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +/** Fallback page count: count markdown page separators if docInfo is absent. */ +function countMarkdownPages(markdown: string): number { + const matches = markdown.match(/\n---\n/g); + return matches !== null ? matches.length + 1 : 1; +} diff --git a/hub/src/capability/mineruClient.ts b/hub/src/capability/mineruClient.ts deleted file mode 100644 index 7628a44..0000000 --- a/hub/src/capability/mineruClient.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * ADR-0027: MinerU client interface. Isolates the real HTTP client so the - * adapter is testable without network access. The real implementation (filling - * in actual MinerU API calls) is deferred until credentials and pricing are - * confirmed; the interface and a mock implementation land now so the adapter - * wiring, UsageFact attribution, and workspace containment are provable. - * - * MinerU cloud API shape (from mineru.net docs / GitHub README): - * - REST API, async task endpoint POST /tasks (v3.0+) + sync POST /file_parse - * - Auth: Bearer token (apiToken) - * - Output: Markdown + extracted images, returned as a zip or structured JSON - * - Cost: reported per-page (pricing requires account confirmation) - * - * The interface models the synchronous parse path for simplicity; the real - * client may poll the async endpoint internally and is free to do so behind - * this signature. - */ -import type { CapabilitySecretPayload } from "./types.js"; - -/** A single extracted image from the parsed document. */ -export interface MineruExtractedImage { - /** Suggested relative filename (e.g. "page_1_fig_0.jpg"). */ - readonly filename: string; - /** Raw image bytes. */ - readonly data: Uint8Array; -} - -/** The structured result of parsing one document. */ -export interface MineruParseResult { - /** Markdown text with image references (relative to output dir). */ - readonly markdown: string; - /** Images extracted from the document, to be written alongside the md. */ - readonly images: readonly MineruExtractedImage[]; - /** Number of pages processed (for UsageFact quantity, unit "pages"). */ - readonly pageCount: number; - /** USD cost if the API reported it; null if unknown (ADR-0022). */ - readonly costUsd: number | null; - /** External task/request id for the UsageFact correlationId. */ - readonly requestId: string | null; -} - -/** Options passed to the client. */ -export interface MineruParseOptions { - /** Absolute path to the input PDF on the Hub's filesystem. */ - readonly inputFilePath: string; -} - -/** - * Client interface for the MinerU document parsing service. The real - * implementation makes authenticated HTTP calls; tests inject a mock. - */ -export interface MineruClient { - parse(credential: CapabilitySecretPayload, options: MineruParseOptions): Promise; -} - -/** Errors raised by the MinerU client. */ -export class MineruClientError extends Error { - constructor( - message: string, - readonly code: "mineru_unreachable" | "mineru_rejected" | "mineru_invalid_response" | "mineru_no_output", - readonly upstreamStatus?: number, - ) { - super(message); - this.name = "MineruClientError"; - } -} diff --git a/hub/src/capability/pdfToMdBundle.ts b/hub/src/capability/pdfToMdBundle.ts index f80d27d..2b096ff 100644 --- a/hub/src/capability/pdfToMdBundle.ts +++ b/hub/src/capability/pdfToMdBundle.ts @@ -20,7 +20,7 @@ import { join, resolve, relative, isAbsolute } from "node:path"; import type { PrismaClient } from "@prisma/client"; import { LocalSecretEnvelope } from "../security/secretEnvelope.js"; import { resolveCapabilityCredential } from "./capabilityConnections.js"; -import { MineruClientError, type MineruClient } from "./mineruClient.js"; +import { DocmindClientError, type CapabilityProviderClient } from "./docmindClient.js"; import { CAPABILITIES, type CapabilityAdapter, @@ -30,7 +30,7 @@ import { } from "./types.js"; const CAPABILITY_ID = "pdf_to_md_bundle" as const; -const PROVIDER_ID = "mineru"; +const PROVIDER_ID = "aliyun_docmind"; /** Thrown when a requested path escapes the workspace root (ADR-0018). */ export class CapabilityPathEscape extends Error { @@ -59,11 +59,11 @@ function confineToWorkspace(requestedPath: string, workspaceDir: string): string export interface PdfToMdBundleDeps { readonly secrets: LocalSecretEnvelope; - readonly client: MineruClient; + readonly client: CapabilityProviderClient; readonly prisma: PrismaClient; } -/** Build the pdf_to_md_bundle adapter. The MineruClient is injectable for testing. */ +/** Build the pdf_to_md_bundle adapter. The client is injectable for testing. */ export function createPdfToMdBundleAdapter(deps: PdfToMdBundleDeps): CapabilityAdapter { return { capabilityId: CAPABILITY_ID, @@ -85,14 +85,14 @@ export function createPdfToMdBundleAdapter(deps: PdfToMdBundleDeps): CapabilityA try { result = await deps.client.parse(credential, { inputFilePath: absoluteInput }); } catch (e) { - if (e instanceof MineruClientError) throw e; - throw new MineruClientError( + if (e instanceof DocmindClientError) throw e; + throw new DocmindClientError( e instanceof Error ? e.message : String(e), - "mineru_unreachable", + "docmind_unreachable", ); } if (result.markdown === "") { - throw new MineruClientError("MinerU returned empty markdown", "mineru_no_output"); + throw new DocmindClientError("docmind returned empty markdown", "docmind_no_output"); } // 4. Write outputs into the workspace. diff --git a/hub/src/capability/types.ts b/hub/src/capability/types.ts index 87521d1..29c5c7a 100644 --- a/hub/src/capability/types.ts +++ b/hub/src/capability/types.ts @@ -80,12 +80,13 @@ export interface CapabilityAdapter { invoke(input: CapabilityInvocationInput): Promise; } -/** Decrypted capability credential (CapabilitySecretPayloadV1, ADR-0027). */ +/** Decrypted capability credential (CapabilitySecretPayloadV1, ADR-0027). + * Alibaba Cloud Document Mind (docmind) uses AccessKey ID + Secret + endpoint. */ export interface CapabilitySecretPayload { readonly schemaVersion: 1; - readonly baseUrl: string; - readonly apiToken: string; - readonly projectId: string | null; + readonly accessKeyId: string; + readonly accessKeySecret: string; + readonly endpoint: string; } /** Thrown when an org has no ACTIVE capability connection (fail-closed, ADR-0024). */ diff --git a/hub/test/integration/capability-pdf-to-md.test.ts b/hub/test/integration/capability-pdf-to-md.test.ts index 9bc484e..00f7af1 100644 --- a/hub/test/integration/capability-pdf-to-md.test.ts +++ b/hub/test/integration/capability-pdf-to-md.test.ts @@ -4,11 +4,11 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { prisma, resetDb, seedTestOrganization, testSecretEnvelope, DEFAULT_ORG_ID } from "./helpers.js"; import { createPdfToMdBundleAdapter, CapabilityPathEscape } from "../../src/capability/pdfToMdBundle.js"; -import { MineruClientError, type MineruClient, type MineruParseResult } from "../../src/capability/mineruClient.js"; +import { DocmindClientError, type CapabilityProviderClient, type DocmindParseResult } from "../../src/capability/docmindClient.js"; import type { CapabilitySecretPayload } from "../../src/capability/types.js"; const CAPABILITY_ID = "pdf_to_md_bundle"; -const PROVIDER_ID = "mineru"; +const PROVIDER_ID = "aliyun_docmind"; /** * ADR-0027: pdf_to_md_bundle adapter contract tests. Proves: @@ -16,8 +16,8 @@ const PROVIDER_ID = "mineru"; * - fail-closed credential resolution (no ACTIVE connection → error) * - UsageFact attribution with non-token meter (pages), costSource rules * - outputs (md + images) written into workspace - * - MinerU client errors propagate - * The MineruClient is mocked; the prisma + envelope + filesystem are real. + * - docmind client errors propagate + * The client is mocked; the prisma + envelope + filesystem are real. */ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { let workspaceRoot: string; @@ -58,9 +58,9 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { async function seedActiveCapabilityConnection(): Promise { const payload: CapabilitySecretPayload = { schemaVersion: 1, - baseUrl: "https://mineru.test/api", - apiToken: "mineru-secret-token", - projectId: null, + accessKeyId: "LTAI-test-key-id", + accessKeySecret: "test-secret-never-log", + endpoint: "docmind-api.cn-hangzhou.aliyuncs.com", }; const connection = await prisma.organizationCapabilityConnection.create({ data: { @@ -95,23 +95,23 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { }); } - function mockMineruClient(result: Partial = {}): MineruClient { - const full: MineruParseResult = { - markdown: "# Parsed Document\n\nHello world.\n\n![fig](page_1_fig_0.jpg)\n", + function mockDocmindClient(result: Partial = {}): CapabilityProviderClient { + const full: DocmindParseResult = { + markdown: "# Parsed Document\n\nHello world.\n\n![fig](page_1.jpg)\n", images: [ - { filename: "page_1_fig_0.jpg", data: new Uint8Array([0xff, 0xd8, 0xff, 0xe0]) }, + { filename: "page_1.jpg", data: new Uint8Array([0xff, 0xd8, 0xff, 0xe0]) }, ], pageCount: 3, - costUsd: 0.015, - requestId: "mineru-task-abc", + costUsd: 0.0168, + requestId: "docmind-job-abc", ...result, }; return { - parse: vi.fn(async (): Promise => full), + parse: vi.fn(async (): Promise => full), }; } - function makeAdapter(client: MineruClient) { + function makeAdapter(client: CapabilityProviderClient) { return createPdfToMdBundleAdapter({ secrets: testSecretEnvelope, client, @@ -130,7 +130,7 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { it("writes md + images into the workspace and records a UsageFact", async () => { await seedActiveCapabilityConnection(); const inputPath = await seedInputPdf(); - const client = mockMineruClient(); + const client = mockDocmindClient(); const adapter = makeAdapter(client); const result = await adapter.invoke({ @@ -147,12 +147,12 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { const md = await readFile(join(workspaceRoot, "output", "document.md"), "utf8"); expect(md).toContain("# Parsed Document"); expect(result.artifacts.some((a) => a.kind === "markdown" && a.path === "output/document.md")).toBe(true); - expect(result.artifacts.some((a) => a.kind === "image" && a.path === "output/page_1_fig_0.jpg")).toBe(true); + expect(result.artifacts.some((a) => a.kind === "image" && a.path === "output/page_1.jpg")).toBe(true); // Client received the decrypted credential, not the env. const call = (client.parse as ReturnType).mock.calls[0]?.[0] as CapabilitySecretPayload; - expect(call.apiToken).toBe("mineru-secret-token"); - expect(call.baseUrl).toBe("https://mineru.test/api"); + expect(call.accessKeyId).toBe("LTAI-test-key-id"); + expect(call.endpoint).toBe("docmind-api.cn-hangzhou.aliyuncs.com"); // UsageFact written with non-token meter and provider_reported cost. const facts = await prisma.usageFact.findMany({ where: { runId } }); @@ -163,15 +163,15 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { expect(fact.capabilityId).toBe(CAPABILITY_ID); expect(Number(fact.quantity)).toBe(3); expect(fact.unit).toBe("pages"); - expect(Number(fact.costUsd!)).toBeCloseTo(0.015, 8); + expect(Number(fact.costUsd!)).toBeCloseTo(0.0168, 8); expect(fact.costSource).toBe("provider_reported"); - expect(fact.correlationId).toBe("mineru-task-abc"); + expect(fact.correlationId).toBe("docmind-job-abc"); }); - it("writes UsageFact with costSource=unknown when MinerU reports no cost", async () => { + it("writes UsageFact with costSource=unknown when docmind reports no cost", async () => { await seedActiveCapabilityConnection(); const inputPath = await seedInputPdf(); - const client = mockMineruClient({ costUsd: null, requestId: null }); + const client = mockDocmindClient({ costUsd: null, requestId: null }); const adapter = makeAdapter(client); await adapter.invoke({ @@ -194,7 +194,7 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { it("fails closed when the org has no ACTIVE capability connection", async () => { // No connection seeded. const inputPath = await seedInputPdf(); - const adapter = makeAdapter(mockMineruClient()); + const adapter = makeAdapter(mockDocmindClient()); await expect(adapter.invoke({ runId, @@ -213,7 +213,7 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { it("rejects an input path that escapes the workspace", async () => { await seedActiveCapabilityConnection(); - const adapter = makeAdapter(mockMineruClient()); + const adapter = makeAdapter(mockDocmindClient()); await expect(adapter.invoke({ runId, @@ -229,7 +229,7 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { it("rejects an output path that escapes the workspace", async () => { await seedActiveCapabilityConnection(); const inputPath = await seedInputPdf(); - const adapter = makeAdapter(mockMineruClient()); + const adapter = makeAdapter(mockDocmindClient()); await expect(adapter.invoke({ runId, @@ -242,12 +242,12 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { })).rejects.toBeInstanceOf(CapabilityPathEscape); }); - it("propagates MinerU client errors without writing a UsageFact", async () => { + it("propagates docmind client errors without writing a UsageFact", async () => { await seedActiveCapabilityConnection(); const inputPath = await seedInputPdf(); - const client: MineruClient = { - parse: vi.fn(async (): Promise => { - throw new MineruClientError("upstream 502", "mineru_rejected", 502); + const client: CapabilityProviderClient = { + parse: vi.fn(async (): Promise => { + throw new DocmindClientError("upstream 502", "docmind_rejected", 502); }), }; const adapter = makeAdapter(client); @@ -269,7 +269,7 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { it("rejects empty markdown output as a parse failure", async () => { await seedActiveCapabilityConnection(); const inputPath = await seedInputPdf(); - const client = mockMineruClient({ markdown: "" }); + const client = mockDocmindClient({ markdown: "" }); const adapter = makeAdapter(client); await expect(adapter.invoke({