-
Notifications
You must be signed in to change notification settings - Fork 13
feat(international-organization-data): add WHO and FAO data connector #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
xSharkM
wants to merge
5
commits into
makecindy:main
Choose a base branch
from
xSharkM:feat/international-organization-data
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
1c6ea45
feat(international-organization-data): add WHO and FAO data connector
xSharkM 856c5c7
fix(international-organization-data): stabilize recent queries
xSharkM ad470f8
fix(international-organization-data): address review feedback
xSharkM 6981c9b
fix(international-organization-data): skip empty WHO observations
xSharkM 2c9b834
fix(international-organization-data): address remaining review issues
xSharkM File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| import test from 'node:test'; | ||
| import assert from 'node:assert/strict'; | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
| import vm from 'node:vm'; | ||
|
|
||
| const root = path.resolve('international-organization-data'); | ||
| const manifest = JSON.parse(fs.readFileSync(path.join(root, 'ghost.json'), 'utf8')); | ||
| const runtimeSource = fs.readFileSync(path.join(root, 'main.js'), 'utf8'); | ||
|
|
||
| function loadRuntime(fetchImpl) { | ||
| let handler; | ||
| const messages = []; | ||
| vm.runInNewContext(runtimeSource, { | ||
| isFinite, | ||
| encodeURIComponent, | ||
| String, | ||
| Math, | ||
| JSON, | ||
| Date, | ||
| Promise, | ||
| setTimeout, | ||
| cindy: { | ||
| onHostMessage(callback) { | ||
| handler = callback; | ||
| }, | ||
| send(message) { | ||
| messages.push(message); | ||
| }, | ||
| fetch: fetchImpl, | ||
| }, | ||
| }); | ||
| return { | ||
| async call(tool, args) { | ||
| messages.length = 0; | ||
| await handler({ type: 'tool-call', callId: tool, tool, args }); | ||
| return messages[0]; | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| test('international organization data manifest is read-only and allowlisted', () => { | ||
| assert.equal(manifest.id, 'international-organization-data'); | ||
| assert.deepEqual(manifest.slots, ['tool', 'network']); | ||
| assert.deepEqual(manifest.network.hosts, [ | ||
| 'ghoapi.azureedge.net', | ||
| 'faostatservices.fao.org', | ||
| 'www.fao.org', | ||
| ]); | ||
| assert.equal(manifest.network.secrets.length, 1); | ||
| assert.equal(manifest.network.secrets[0].key, 'faostat_api_token'); | ||
| assert.equal(manifest.network.secrets[0].inject.hosts[0], 'faostatservices.fao.org'); | ||
| assert.equal(manifest.tools.length, 3); | ||
| assert.ok(manifest.tools.every((tool) => !/send|delete|write|update|create/i.test(tool.description))); | ||
| }); | ||
|
|
||
| test('international organization data includes all four locale resources', () => { | ||
| for (const locale of ['en', 'zh-CN', 'ja', 'ko']) { | ||
| const file = path.join(root, 'locales', `${locale}.json`); | ||
| const resource = JSON.parse(fs.readFileSync(file, 'utf8')); | ||
| assert.equal(resource.name.length > 0, true); | ||
| for (const tool of ['international_org_catalog', 'who_health_data', 'fao_agriculture_data']) { | ||
| assert.equal(typeof resource.tools[tool].description, 'string'); | ||
| assert.ok(resource.tools[tool].description.length > 20); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| test('international organization data does not contain credentials or arbitrary network calls', () => { | ||
| const source = fs.readFileSync(path.join(root, 'main.js'), 'utf8'); | ||
| assert.equal(source.includes('globalThis.fetch'), false); | ||
| assert.equal(source.includes('window.fetch'), false); | ||
| assert.equal(source.includes('process.env'), false); | ||
| assert.equal(source.includes('FAO_TOKEN'), false); | ||
| assert.equal(source.includes('Authorization'), true); | ||
| assert.equal(source.includes('ghoapi.azureedge.net'), true); | ||
| assert.equal(source.includes('faostatservices.fao.org'), true); | ||
| }); | ||
|
|
||
| test('WHO recent queries are ordered and scoped per country', () => { | ||
| const requestedUrls = []; | ||
| const runtime = loadRuntime(async ({ url }) => { | ||
| requestedUrls.push(decodeURIComponent(url)); | ||
| const country = url.includes('CHN') ? 'CHN' : 'USA'; | ||
| return { | ||
| ok: true, | ||
| status: 200, | ||
| body: JSON.stringify({ | ||
| value: [{ | ||
| IndicatorCode: 'WHOSIS_000001', | ||
| SpatialDim: country, | ||
| TimeDim: 2021, | ||
| NumericValue: country === 'CHN' ? 77.6 : 76.4, | ||
| Value: country === 'CHN' ? '77.6' : '76.4', | ||
| Dim1Type: 'SEX', | ||
| Dim1: 'SEX_BTSX', | ||
| }], | ||
| }), | ||
| }; | ||
| }); | ||
|
|
||
| return runtime.call('who_health_data', { | ||
| indicator: 'life_expectancy', | ||
| countries: ['CHN', 'USA'], | ||
| recent: 1, | ||
| limit: 1, | ||
| }).then((result) => { | ||
| assert.equal(result.ok, true); | ||
| assert.deepEqual( | ||
| Array.from(result.result.rows, (row) => row.country), | ||
| ['CHN', 'USA'], | ||
| ); | ||
| assert.equal(requestedUrls.length, 2); | ||
| assert.ok(requestedUrls.every((url) => url.includes('$orderby=TimeDim desc'))); | ||
| assert.equal(requestedUrls.filter((url) => url.includes("SpatialDim eq 'CHN'")).length, 1); | ||
| assert.equal(requestedUrls.filter((url) => url.includes("SpatialDim eq 'USA'")).length, 1); | ||
| }); | ||
| }); | ||
|
|
||
| test('FAOSTAT auth failures have actionable guidance', async () => { | ||
| const runtime = loadRuntime(async () => ({ | ||
| ok: false, | ||
| status: 403, | ||
| body: '{"message":"Forbidden"}', | ||
| message: 'HTTP 403', | ||
| })); | ||
| const result = await runtime.call('fao_agriculture_data', { | ||
| domainCode: 'QCL', | ||
| area: '351', | ||
| }); | ||
| assert.equal(result.ok, false); | ||
| assert.match(result.message, /HTTP 403/); | ||
| assert.match(result.message, /检查 Token 是否有效、权限是否已开通/); | ||
| }); |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| { | ||
| "schemaVersion": 2, | ||
| "id": "international-organization-data", | ||
| "name": "International Organization Data", | ||
| "description": "查询 WHO 与 FAO 的官方健康、农业、粮食与食品安全统计数据。WHO 公共数据无需 API Key;FAOSTAT 数据需要在插件设置页配置官方 API Token。插件只读,不修改任何外部数据。", | ||
| "whenToUse": "需要查询 WHO 全球健康指标,或 FAO 农业、粮食、食品安全、贸易与生产统计时使用。结果会保留数据源、指标、单位、统计年份与更新时间;它不是实时监测、个人医疗诊断或投资建议工具。", | ||
| "version": "0.1.0", | ||
| "locales": { | ||
| "en": "locales/en.json", | ||
| "zh-CN": "locales/zh-CN.json", | ||
| "ja": "locales/ja.json", | ||
| "ko": "locales/ko.json" | ||
| }, | ||
| "author": "Cindy", | ||
| "icon": "assets/icon.png", | ||
| "entry": "main.js", | ||
| "settingsHtml": "settings.html", | ||
| "slots": [ | ||
| "tool", | ||
| "network" | ||
| ], | ||
| "command": "international-organization-data", | ||
| "network": { | ||
| "hosts": [ | ||
| "ghoapi.azureedge.net", | ||
| "faostatservices.fao.org", | ||
| "www.fao.org" | ||
| ], | ||
| "secrets": [ | ||
| { | ||
| "key": "faostat_api_token", | ||
| "label": "FAOSTAT API Token", | ||
| "hint": "可选;用于查询 FAOSTAT 数据", | ||
| "url": "https://www.fao.org/faostat/en/#developer-portal", | ||
| "inject": { | ||
| "header": "Authorization", | ||
| "format": "Bearer {value}", | ||
| "hosts": [ | ||
| "faostatservices.fao.org" | ||
| ] | ||
| } | ||
| } | ||
| ] | ||
| }, | ||
| "tools": [ | ||
| { | ||
| "name": "international_org_catalog", | ||
| "description": "浏览 WHO 或 FAO 的官方数据目录。source=who 时可搜索健康指标或列出国家;source=fao 时可列出数据组、数据域、维度和过滤代码。FAO 目录操作需要先在插件设置页配置 API Token。", | ||
| "parameters": { | ||
| "type": "object", | ||
| "properties": { | ||
| "source": { | ||
| "type": "string", | ||
| "enum": [ | ||
| "who", | ||
| "fao" | ||
| ], | ||
| "description": "数据源:who 或 fao" | ||
| }, | ||
| "action": { | ||
| "type": "string", | ||
| "enum": [ | ||
| "common", | ||
| "indicators", | ||
| "countries", | ||
| "groups", | ||
| "domains", | ||
| "dimensions", | ||
| "codes" | ||
| ], | ||
| "description": "WHO 支持 common、indicators、countries;FAO 支持 groups、domains、dimensions、codes。默认 common。" | ||
| }, | ||
| "query": { | ||
| "type": "string", | ||
| "description": "WHO 指标或国家搜索词;不传时返回常用目录" | ||
| }, | ||
| "groupCode": { | ||
| "type": "string", | ||
| "description": "FAO action=domains 时的数据组代码,例如 Q、T" | ||
| }, | ||
| "domainCode": { | ||
| "type": "string", | ||
| "description": "FAO action=dimensions/codes 时的数据域代码,例如 QCL、FS、T" | ||
| }, | ||
| "dimension": { | ||
| "type": "string", | ||
| "description": "FAO action=codes 时的维度,例如 area、item、element、year" | ||
| }, | ||
| "limit": { | ||
| "type": "number", | ||
| "description": "最多返回多少项,默认 20,最大 100" | ||
| } | ||
| }, | ||
| "required": [ | ||
| "source" | ||
| ] | ||
| } | ||
| }, | ||
| { | ||
| "name": "who_health_data", | ||
| "description": "查询 WHO 全球健康指标。indicator 可传 WHO 指标代码或常用别名;countries 使用 ISO3 国家代码;可选年份范围。不传年份范围时必须提供 countries,插件会按国家分别查询并返回每个国家最近有效值,最终最多返回 countries 数量 × recent 条,避免全局截断遗漏国家。结果带国家、年份、数值、单位、原始值与 WHO 更新时间。只读,无需 API Key。健康统计仅供信息参考,不用于个人诊断。", | ||
| "parameters": { | ||
| "type": "object", | ||
| "properties": { | ||
| "indicator": { | ||
| "type": "string", | ||
| "description": "WHO 指标代码或常用别名,例如 life_expectancy、under5_mortality、maternal_mortality" | ||
| }, | ||
| "countries": { | ||
| "type": "array", | ||
| "items": { | ||
| "type": "string" | ||
| }, | ||
| "description": "ISO3 国家代码数组,最多 25 个,例如 [\"CHN\", \"USA\"];不传年份范围时必填" | ||
| }, | ||
| "startYear": { | ||
| "type": "number", | ||
| "description": "可选,起始年份" | ||
| }, | ||
| "endYear": { | ||
| "type": "number", | ||
| "description": "可选,结束年份" | ||
| }, | ||
| "recent": { | ||
| "type": "number", | ||
| "description": "不传年份范围时,每个国家返回最近多少期有效值,默认 1,最大 20" | ||
| }, | ||
| "limit": { | ||
| "type": "number", | ||
| "description": "传年份范围时是单次返回上限;不传年份时是每个国家向 WHO 请求的候选行上限,最终每国最多返回 recent 条。默认 50,最大 200" | ||
| } | ||
| }, | ||
| "required": [ | ||
| "indicator" | ||
| ] | ||
| } | ||
| }, | ||
| { | ||
| "name": "fao_agriculture_data", | ||
| "description": "查询 FAOSTAT 农业、粮食、食品安全、贸易等官方统计。先用 international_org_catalog 查询数据域、维度和代码,再传入 domainCode 及 area/item/element/year 过滤条件。FAOSTAT 需要在插件设置页配置官方 API Token;必须至少提供一个过滤条件以避免返回过大结果。只读。", | ||
| "parameters": { | ||
| "type": "object", | ||
| "properties": { | ||
| "domainCode": { | ||
| "type": "string", | ||
| "description": "FAO 数据域代码,例如 QCL(作物与畜牧产品)、FS(食品安全)、T(贸易)" | ||
| }, | ||
| "area": { | ||
| "type": "string", | ||
| "description": "国家/地区代码,逗号分隔;先通过 catalog action=codes 查询 area 代码" | ||
| }, | ||
| "item": { | ||
| "type": "string", | ||
| "description": "商品或指标项目代码,逗号分隔;先查询 item 代码" | ||
| }, | ||
| "element": { | ||
| "type": "string", | ||
| "description": "统计要素过滤代码,逗号分隔;先查询 element 代码" | ||
| }, | ||
| "year": { | ||
| "type": "string", | ||
| "description": "年份代码,逗号分隔,例如 2020,2021,2022" | ||
| }, | ||
| "limit": { | ||
| "type": "number", | ||
| "description": "最多返回多少条,默认 50,最大 200" | ||
| }, | ||
| "showCodes": { | ||
| "type": "boolean", | ||
| "description": "是否包含 FAO 代码字段,默认 false" | ||
| } | ||
| }, | ||
| "required": [ | ||
| "domainCode" | ||
| ] | ||
| } | ||
| } | ||
| ] | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "name": "International Organization Data", | ||
| "description": "Query official WHO and FAO health, agriculture, food, and food-security statistics. WHO needs no API key; FAOSTAT requires an official API token configured in the plugin settings.", | ||
| "whenToUse": "Use when the user asks for WHO health indicators or FAO agriculture, food-security, trade, and production statistics. Results include the source, indicator, unit, statistical year, and update time; this is not a personal medical diagnosis tool.", | ||
| "tools": { | ||
| "international_org_catalog": { | ||
| "description": "Browse official WHO or FAO data catalogs. WHO supports common indicators, health indicators, and countries; FAO supports groups, domains, dimensions, and filter codes. FAO catalog actions require an API token." | ||
| }, | ||
| "who_health_data": { | ||
| "description": "Query WHO global health indicators by WHO indicator code or common alias, ISO3 country codes, and optional year range. Without a year range, provide countries so the plugin can query each country separately and return up to countries × recent deterministic non-empty observations without global truncation. No API key is required; informational only, not personal medical diagnosis." | ||
| }, | ||
| "fao_agriculture_data": { | ||
| "description": "Query official FAOSTAT agriculture, food, food-security, trade, and production statistics. Discover domains and filter codes first; configure an official API token in plugin settings and provide at least one filter. Read-only." | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "name": "国際機関公式データ", | ||
| "description": "WHO と FAO の公式な健康、農業、食料、食料安全保障統計を検索します。WHO は API キー不要、FAOSTAT はプラグイン設定で公式 API トークンを設定します。", | ||
| "whenToUse": "WHO の健康指標、または FAO の農業、食料、食料安全保障、貿易、生産統計を調べるときに使用します。結果にはデータソース、指標、単位、統計年、更新時刻を含めます。個人の診断には使用しません。", | ||
| "tools": { | ||
| "international_org_catalog": { | ||
| "description": "WHO または FAO の公式データカタログを参照します。WHO は一般的な指標、健康指標、国を、FAO はグループ、ドメイン、ディメンション、フィルターコードを扱います。FAO には API トークンが必要です。" | ||
| }, | ||
| "who_health_data": { | ||
| "description": "WHO の健康指標を指標コードまたは別名、ISO3 国コード、任意の年範囲で検索します。年範囲を指定しない場合は国コードが必要で、国ごとに別々に検索して最大で国数 × recent 件の最新の空でない観測値を返し、全体の切り捨てによる欠落を防ぎます。API キー不要で、情報提供のみです。" | ||
| }, | ||
| "fao_agriculture_data": { | ||
| "description": "FAOSTAT の農業、食料、食料安全保障、貿易、生産統計を検索します。先にドメインとフィルターコードを確認し、設定で公式 API トークンを指定してから、少なくとも一つのフィルターを渡します。読み取り専用です。" | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "name": "국제기구 공식 데이터", | ||
| "description": "WHO와 FAO의 공식 보건, 농업, 식량 및 식량안보 통계를 조회합니다. WHO는 API 키가 필요 없고 FAOSTAT은 플러그인 설정에서 공식 API 토큰을 설정해야 합니다.", | ||
| "whenToUse": "WHO 보건 지표 또는 FAO 농업, 식량안보, 무역, 생산 통계를 조회할 때 사용합니다. 결과에는 출처, 지표, 단위, 통계 연도와 업데이트 시간이 포함되며 개인 의료 진단에는 사용하지 않습니다.", | ||
| "tools": { | ||
| "international_org_catalog": { | ||
| "description": "WHO 또는 FAO 공식 데이터 카탈로그를 탐색합니다. WHO는 공통 지표, 보건 지표와 국가를 지원하고 FAO는 그룹, 도메인, 차원과 필터 코드를 지원합니다. FAO 카탈로그에는 API 토큰이 필요합니다." | ||
| }, | ||
| "who_health_data": { | ||
| "description": "WHO 글로벌 보건 지표를 WHO 코드 또는 별칭, ISO3 국가 코드와 선택적 연도 범위로 조회합니다. 연도 범위를 생략하면 국가 코드를 제공해야 하며 국가별로 따로 조회해 최대 국가 수 × recent개의 최신 비어 있지 않은 관측값을 반환하고 전체 잘림으로 국가가 누락되지 않도록 합니다. API 키가 필요 없으며 정보 제공용입니다." | ||
| }, | ||
| "fao_agriculture_data": { | ||
| "description": "FAOSTAT 공식 농업, 식량, 식량안보, 무역 및 생산 통계를 조회합니다. 먼저 도메인과 필터 코드를 확인하고 설정에서 공식 API 토큰을 구성한 뒤 하나 이상의 필터를 전달해야 합니다. 읽기 전용입니다." | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "name": "国际组织官方数据", | ||
| "description": "查询 WHO 与 FAO 的官方健康、农业、粮食与食品安全统计数据。WHO 无需 API Key;FAOSTAT 需要在插件设置页配置官方 API Token。", | ||
| "whenToUse": "需要查询 WHO 全球健康指标,或 FAO 农业、粮食、食品安全、贸易与生产统计时使用。结果带数据源、指标、单位、统计年份与更新时间;不用于个人医疗诊断。", | ||
| "tools": { | ||
| "international_org_catalog": { | ||
| "description": "浏览 WHO 或 FAO 的官方数据目录。WHO 支持常用指标、健康指标和国家目录;FAO 支持数据组、数据域、维度和过滤代码。FAO 目录需要先配置 API Token。" | ||
| }, | ||
| "who_health_data": { | ||
| "description": "查询 WHO 全球健康指标。可使用 WHO 指标代码或常用别名,并按 ISO3 国家代码和年份筛选。不传年份时必须提供国家代码,插件会按国家分别查询最近有效值,最终最多返回国家数 × recent 条,避免全局截断遗漏国家。无需 API Key,仅供信息参考,不用于个人医疗诊断。" | ||
| }, | ||
| "fao_agriculture_data": { | ||
| "description": "查询 FAOSTAT 农业、粮食、食品安全、贸易等官方统计。先查数据域和过滤代码,再传入筛选条件;需要在插件设置页配置官方 API Token,且至少提供一个过滤条件。只读。" | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.