Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions .tests/international-organization-data.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
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.ok(requestedUrls.every((url) => url.includes("Dim1 eq 'SEX_BTSX'")));
assert.ok(requestedUrls.every((url) => url.includes('(NumericValue ne null or Value ne null)')));
assert.equal(requestedUrls.filter((url) => url.includes("SpatialDim eq 'CHN'")).length, 1);
assert.equal(requestedUrls.filter((url) => url.includes("SpatialDim eq 'USA'")).length, 1);
assert.equal(Object.hasOwn(result.result.rows[0], 'unit'), false);
assert.equal(result.result.rows[0].dimensionType, 'SEX');
});
});

test('WHO recent queries do not let empty newer observations consume the result', async () => {
const runtime = loadRuntime(async () => ({
ok: true,
status: 200,
body: JSON.stringify({
value: [
{
IndicatorCode: 'WHOSIS_000001', SpatialDim: 'CHN', TimeDim: 2022,
NumericValue: null, Value: null, Dim1Type: 'SEX', Dim1: 'SEX_BTSX',
},
{
IndicatorCode: 'WHOSIS_000001', SpatialDim: 'CHN', TimeDim: 2021,
NumericValue: 77.6, Value: '77.6', Dim1Type: 'SEX', Dim1: 'SEX_BTSX',
},
],
}),
}));

const result = await runtime.call('who_health_data', {
indicator: 'life_expectancy',
countries: ['CHN'],
recent: 1,
});
assert.equal(result.ok, true);
assert.equal(result.result.rows.length, 1);
assert.equal(result.result.rows[0].year, 2021);
assert.equal(result.result.rows[0].value, 77.6);
});

test('settings page follows the host locale with an English fallback', () => {
const html = fs.readFileSync(path.join(root, 'settings.html'), 'utf8');
const source = fs.readFileSync(path.join(root, 'settings.js'), 'utf8');
assert.match(html, /data-i18n="tokenTitle"/);
assert.match(html, /data-i18n-placeholder="tokenPlaceholder"/);
assert.match(source, /fetch\('\/app-context'/);
for (const locale of ['en', 'zh-CN', 'ja', 'ko']) {
assert.ok(source.includes(locale));
}
});

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 是否有效、权限是否已开通/);
});
Binary file added international-organization-data/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
179 changes: 179 additions & 0 deletions international-organization-data/ghost.json
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"
Comment thread
xSharkM marked this conversation as resolved.
Outdated
],
"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 更新时间;不会把 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"
]
}
}
]
}
16 changes: 16 additions & 0 deletions international-organization-data/locales/en.json
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, statistical year, original display value, 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."
}
}
}
16 changes: 16 additions & 0 deletions international-organization-data/locales/ja.json
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 トークンを指定してから、少なくとも一つのフィルターを渡します。読み取り専用です。"
}
}
}
Loading