@rivus/agent 0.16.6 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -21
- package/README.md +23 -2
- package/dist/cli.d.ts +1 -1
- package/dist/index.d.ts +3 -12
- package/dist/index.js +3 -5
- package/dist/testing/index.d.ts +21 -50
- package/dist/testing/index.js +58 -89
- package/package.json +14 -41
- package/dist/acp.d.ts +0 -2
- package/dist/acp.js +0 -2
- package/dist/bootstrap/pi-feishu.d.ts +0 -2
- package/dist/bootstrap/pi-feishu.js +0 -2
- package/dist/chunks/rivus-plugin-testkit.d.ts +0 -27
- package/dist/chunks/rivus-plugin-testkit.js +0 -63
- package/dist/mcp.d.ts +0 -2
- package/dist/mcp.js +0 -6
- package/dist/pi.d.ts +0 -2
- package/dist/pi.js +0 -2
- package/examples/a-share-briefing-analysis.mjs +0 -93
- package/examples/a-share-briefing-renderer.mjs +0 -257
- package/examples/a-share-index-evidence.mjs +0 -99
- package/examples/a-share-market-briefing.mjs +0 -83
- package/examples/a-share-market-date.mjs +0 -10
- package/examples/a-share-overseas-evidence.mjs +0 -86
- package/examples/a-share-policy-evidence.mjs +0 -145
- package/examples/a-share-provider-response.mjs +0 -21
- package/examples/a-share-sector-evidence.mjs +0 -70
- package/examples/acp-stdio-proxy.mjs +0 -58
- package/examples/current-weather.mjs +0 -117
- package/examples/html-drive-tools.mjs +0 -262
- package/examples/https-response-reader.mjs +0 -36
- package/examples/langfuse-drive-e2e.mjs +0 -175
- package/examples/pi-feishu-deployment.bootstrap.ts +0 -3
- package/examples/pi-feishu.bootstrap.ts +0 -242
- package/examples/rivus-agents.plugin.mjs +0 -290
- package/examples/rivus-langfuse-demo.config.json +0 -37
- package/examples/rivus-starter.plugin.mjs +0 -47
- package/examples/rivus.config.json +0 -114
- package/skills/runtime-management/SKILL.md +0 -65
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { readAShareProviderResponse } from "./a-share-provider-response.mjs";
|
|
2
|
-
|
|
3
|
-
const SOURCE = "腾讯证券海外指数公开行情";
|
|
4
|
-
const MAX_AGE_MS = 4 * 24 * 60 * 60 * 1000;
|
|
5
|
-
const INDEXES = Object.freeze([
|
|
6
|
-
{ name: "道琼斯", symbol: "usDJI" },
|
|
7
|
-
{ name: "纳斯达克", symbol: "usIXIC" },
|
|
8
|
-
{ name: "标普 500", symbol: "usINX" }
|
|
9
|
-
]);
|
|
10
|
-
|
|
11
|
-
export async function readAShareOverseasEvidence({ occurrence }) {
|
|
12
|
-
const url = new URL("https://qt.gtimg.cn/");
|
|
13
|
-
url.searchParams.set("q", INDEXES.map(({ symbol }) => symbol).join(","));
|
|
14
|
-
const responseBody = await readAShareProviderResponse({
|
|
15
|
-
accept: "text/plain",
|
|
16
|
-
maxBytes: 64 * 1024,
|
|
17
|
-
referer: "https://finance.qq.com/",
|
|
18
|
-
source: "a-share overseas evidence",
|
|
19
|
-
url
|
|
20
|
-
});
|
|
21
|
-
const body = new TextDecoder("gbk").decode(responseBody);
|
|
22
|
-
const quotes = INDEXES.map((index) => parseQuote(index, body));
|
|
23
|
-
const asOf = quotes
|
|
24
|
-
.map(({ providerTime }) => providerTime)
|
|
25
|
-
.sort((left, right) => left.localeCompare(right))
|
|
26
|
-
.at(-1);
|
|
27
|
-
if (new Set(quotes.map(({ providerTime }) => providerTime)).size !== 1) {
|
|
28
|
-
throw new Error("a-share overseas evidence timestamps are inconsistent");
|
|
29
|
-
}
|
|
30
|
-
const age = providerClockTime(occurrence) - providerClockTime(asOf);
|
|
31
|
-
if (age < 0 || age > MAX_AGE_MS) throw new Error("a-share overseas evidence is stale for the occurrence");
|
|
32
|
-
return Object.freeze({ asOf, quotes: Object.freeze(quotes), source: SOURCE });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function providerClockTime(value) {
|
|
36
|
-
if (value.includes("T")) {
|
|
37
|
-
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
38
|
-
day: "2-digit",
|
|
39
|
-
hour: "2-digit",
|
|
40
|
-
hourCycle: "h23",
|
|
41
|
-
minute: "2-digit",
|
|
42
|
-
month: "2-digit",
|
|
43
|
-
second: "2-digit",
|
|
44
|
-
timeZone: "America/New_York",
|
|
45
|
-
year: "numeric"
|
|
46
|
-
}).formatToParts(new Date(value));
|
|
47
|
-
const read = (type) => parts.find((part) => part.type === type)?.value;
|
|
48
|
-
return Date.parse(
|
|
49
|
-
`${read("year")}-${read("month")}-${read("day")}T${read("hour")}:${read("minute")}:${read("second")}Z`
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
return Date.parse(`${value.replace(" ", "T")}Z`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function parseQuote(index, body) {
|
|
56
|
-
const match = body.match(new RegExp(`(?:^|\\n)v_${index.symbol}="([^"]+)";`, "u"));
|
|
57
|
-
if (!match) throw new Error(`a-share overseas evidence is missing ${index.symbol}`);
|
|
58
|
-
const fields = match[1].split("~");
|
|
59
|
-
const providerTime = fields[30];
|
|
60
|
-
if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(providerTime ?? "")) {
|
|
61
|
-
throw new Error(`a-share overseas evidence is missing a timestamp for ${index.symbol}`);
|
|
62
|
-
}
|
|
63
|
-
if (!isValidProviderTimestamp(providerTime)) {
|
|
64
|
-
throw new Error(`a-share overseas evidence has an invalid timestamp for ${index.symbol}`);
|
|
65
|
-
}
|
|
66
|
-
if (fields[35] !== "USD") throw new Error(`a-share overseas evidence has an unexpected currency for ${index.symbol}`);
|
|
67
|
-
return Object.freeze({
|
|
68
|
-
changePercent: readFiniteNumber(fields[32], index.symbol, "change percent"),
|
|
69
|
-
name: index.name,
|
|
70
|
-
price: readFiniteNumber(fields[3], index.symbol, "price"),
|
|
71
|
-
providerTime,
|
|
72
|
-
symbol: index.symbol
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function isValidProviderTimestamp(value) {
|
|
77
|
-
const normalized = value.replace(" ", "T");
|
|
78
|
-
const timestamp = Date.parse(`${normalized}Z`);
|
|
79
|
-
return Number.isFinite(timestamp) && new Date(timestamp).toISOString().slice(0, 19) === normalized;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function readFiniteNumber(value, symbol, field) {
|
|
83
|
-
const number = Number(value);
|
|
84
|
-
if (!Number.isFinite(number)) throw new Error(`a-share overseas evidence is missing ${field} for ${symbol}`);
|
|
85
|
-
return number;
|
|
86
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { readAShareProviderResponse } from "./a-share-provider-response.mjs";
|
|
2
|
-
|
|
3
|
-
const LOOKBACK_MS = 3 * 24 * 60 * 60 * 1000;
|
|
4
|
-
|
|
5
|
-
export async function readASharePolicyEvidence({ occurrence }) {
|
|
6
|
-
const occurrenceTime = Date.parse(occurrence);
|
|
7
|
-
const readers = [
|
|
8
|
-
{ read: readPbcHeadlines, source: "中国人民银行" },
|
|
9
|
-
{ read: readCsrcHeadlines, source: "中国证监会" }
|
|
10
|
-
];
|
|
11
|
-
const settled = await Promise.allSettled(readers.map(({ read }) => read()));
|
|
12
|
-
const available = settled.flatMap((result, index) =>
|
|
13
|
-
result.status === "fulfilled" ? [{ items: result.value, source: readers[index].source }] : []
|
|
14
|
-
);
|
|
15
|
-
if (available.length === 0) throw new Error("a-share policy evidence sources are unavailable");
|
|
16
|
-
const items = available
|
|
17
|
-
.flatMap(({ items }) => items)
|
|
18
|
-
.filter(({ publishedAt }) => {
|
|
19
|
-
const publishedTime = Date.parse(publishedAt);
|
|
20
|
-
return publishedTime <= occurrenceTime && publishedTime >= occurrenceTime - LOOKBACK_MS;
|
|
21
|
-
})
|
|
22
|
-
.sort((left, right) => right.publishedAt.localeCompare(left.publishedAt))
|
|
23
|
-
.slice(0, 4);
|
|
24
|
-
const sources = available.map(({ source }) => source);
|
|
25
|
-
const unavailableSources = settled.flatMap((result, index) =>
|
|
26
|
-
result.status === "rejected" ? [readers[index].source] : []
|
|
27
|
-
);
|
|
28
|
-
return Object.freeze({
|
|
29
|
-
items: Object.freeze(items),
|
|
30
|
-
sources: Object.freeze(sources),
|
|
31
|
-
unavailableSources: Object.freeze(unavailableSources)
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function readPbcHeadlines() {
|
|
36
|
-
const url = new URL("https://www.pbc.gov.cn/goutongjiaoliu/113456/113469/index.html");
|
|
37
|
-
const body = await readText(url, "中国人民银行");
|
|
38
|
-
const items = [];
|
|
39
|
-
const pattern =
|
|
40
|
-
/<a\s+([^>]*\bistitle="true"[^>]*)>[\s\S]*?<\/a>\s*<\/font>\s*<span[^>]*>\s*(\d{4}-\d{2}-\d{2})\s*<\/span>/giu;
|
|
41
|
-
for (const match of body.matchAll(pattern)) {
|
|
42
|
-
const href = readHtmlAttribute(match[1], "href");
|
|
43
|
-
const title = decodeHtml(readHtmlAttribute(match[1], "title"));
|
|
44
|
-
if (!href || !title || title.length > 200) continue;
|
|
45
|
-
const itemUrl = normalizeSourceUrl(href, url, "www.pbc.gov.cn");
|
|
46
|
-
if (!itemUrl) continue;
|
|
47
|
-
items.push(
|
|
48
|
-
Object.freeze({
|
|
49
|
-
publishedAt: readPbcPublishedAt(itemUrl, match[2]),
|
|
50
|
-
source: "中国人民银行",
|
|
51
|
-
title,
|
|
52
|
-
url: itemUrl
|
|
53
|
-
})
|
|
54
|
-
);
|
|
55
|
-
if (items.length === 2) break;
|
|
56
|
-
}
|
|
57
|
-
if (items.length === 0) throw new Error("PBC policy evidence contains no valid headlines");
|
|
58
|
-
return Object.freeze(items);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function readPbcPublishedAt(itemUrl, displayedDate) {
|
|
62
|
-
const timestamp = new URL(itemUrl).pathname.match(/\/(\d{14})\d*\/index\.html$/u)?.[1];
|
|
63
|
-
if (!timestamp) return `${displayedDate}T23:59:59+08:00`;
|
|
64
|
-
return `${timestamp.replace(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/u, "$1-$2-$3T$4:$5:$6")}+08:00`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function readCsrcHeadlines() {
|
|
68
|
-
const url = new URL("https://www.csrc.gov.cn/searchList/a1a078ee0bc54721ab6b148884c784a8");
|
|
69
|
-
url.searchParams.set("_isAgg", "true");
|
|
70
|
-
url.searchParams.set("_isJson", "true");
|
|
71
|
-
url.searchParams.set("_pageSize", "5");
|
|
72
|
-
url.searchParams.set("_template", "index");
|
|
73
|
-
url.searchParams.set("_rangeTimeGte", "");
|
|
74
|
-
url.searchParams.set("_channelName", "");
|
|
75
|
-
url.searchParams.set("page", "1");
|
|
76
|
-
const body = await readText(url, "中国证监会", 256 * 1024);
|
|
77
|
-
let payload;
|
|
78
|
-
try {
|
|
79
|
-
payload = JSON.parse(body);
|
|
80
|
-
} catch (cause) {
|
|
81
|
-
throw new Error("CSRC policy evidence is not valid JSON", { cause });
|
|
82
|
-
}
|
|
83
|
-
const results = Array.isArray(payload?.data?.results) ? payload.data.results : undefined;
|
|
84
|
-
if (!results) throw new Error("CSRC policy evidence contains no result list");
|
|
85
|
-
const items = results
|
|
86
|
-
.flatMap((row) => {
|
|
87
|
-
if (
|
|
88
|
-
typeof row?.title !== "string" ||
|
|
89
|
-
!row.title ||
|
|
90
|
-
row.title.length > 200 ||
|
|
91
|
-
typeof row?.url !== "string" ||
|
|
92
|
-
!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(row?.publishedTimeStr ?? "")
|
|
93
|
-
) {
|
|
94
|
-
return [];
|
|
95
|
-
}
|
|
96
|
-
const itemUrl = normalizeSourceUrl(row.url, url, "www.csrc.gov.cn");
|
|
97
|
-
if (!itemUrl) return [];
|
|
98
|
-
return [
|
|
99
|
-
Object.freeze({
|
|
100
|
-
publishedAt: `${row.publishedTimeStr.replace(" ", "T")}+08:00`,
|
|
101
|
-
source: "中国证监会",
|
|
102
|
-
title: row.title,
|
|
103
|
-
url: itemUrl
|
|
104
|
-
})
|
|
105
|
-
];
|
|
106
|
-
})
|
|
107
|
-
.slice(0, 2);
|
|
108
|
-
if (items.length === 0) throw new Error("CSRC policy evidence contains no valid headlines");
|
|
109
|
-
return Object.freeze(items);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function readText(url, source, maxBytes = 128 * 1024) {
|
|
113
|
-
const body = await readAShareProviderResponse({
|
|
114
|
-
accept: "text/html,application/json",
|
|
115
|
-
maxBytes,
|
|
116
|
-
referer: `${url.origin}/`,
|
|
117
|
-
source: `${source} policy evidence`,
|
|
118
|
-
url
|
|
119
|
-
});
|
|
120
|
-
return body.toString("utf8");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function readHtmlAttribute(attributes, name) {
|
|
124
|
-
return attributes.match(new RegExp(`\\b${name}="([^"]+)"`, "iu"))?.[1];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function normalizeSourceUrl(value, baseUrl, expectedHostname) {
|
|
128
|
-
let url;
|
|
129
|
-
try {
|
|
130
|
-
url = new URL(value, baseUrl);
|
|
131
|
-
} catch {
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
if (url.protocol !== "https:" || url.hostname !== expectedHostname) return undefined;
|
|
135
|
-
return url.href;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function decodeHtml(value) {
|
|
139
|
-
return value
|
|
140
|
-
.replaceAll("&", "&")
|
|
141
|
-
.replaceAll(""", '"')
|
|
142
|
-
.replaceAll("'", "'")
|
|
143
|
-
.replaceAll("<", "<")
|
|
144
|
-
.replaceAll(">", ">");
|
|
145
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { readBoundedHttpsResponse } from "./https-response-reader.mjs";
|
|
2
|
-
|
|
3
|
-
export async function readAShareProviderResponse({ accept, maxBytes, referer, source, url }) {
|
|
4
|
-
let response;
|
|
5
|
-
try {
|
|
6
|
-
response = await readBoundedHttpsResponse(url, {
|
|
7
|
-
headers: {
|
|
8
|
-
accept,
|
|
9
|
-
referer,
|
|
10
|
-
"user-agent": "rivus-a-share-market-briefing/1.1"
|
|
11
|
-
},
|
|
12
|
-
maxBytes
|
|
13
|
-
});
|
|
14
|
-
} catch (cause) {
|
|
15
|
-
throw new Error(`${source} request failed`, { cause });
|
|
16
|
-
}
|
|
17
|
-
if (response.status < 200 || response.status >= 300) {
|
|
18
|
-
throw new Error(`${source} request failed with HTTP ${response.status}`);
|
|
19
|
-
}
|
|
20
|
-
return response.body;
|
|
21
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { dateInShanghai } from "./a-share-market-date.mjs";
|
|
2
|
-
import { readAShareProviderResponse } from "./a-share-provider-response.mjs";
|
|
3
|
-
|
|
4
|
-
const SOURCE = "东方财富行业板块公开行情";
|
|
5
|
-
|
|
6
|
-
export async function readAShareSectorEvidence({ marketDate }) {
|
|
7
|
-
const [leaders, laggards] = await Promise.all([readSectorRanking(true), readSectorRanking(false)]);
|
|
8
|
-
const sectors = [...leaders, ...laggards];
|
|
9
|
-
const asOf = sectors
|
|
10
|
-
.map(({ asOf }) => asOf)
|
|
11
|
-
.sort((left, right) => left.localeCompare(right))
|
|
12
|
-
.at(-1);
|
|
13
|
-
if (!sectors.every((sector) => dateInShanghai(new Date(sector.asOf)) === marketDate)) {
|
|
14
|
-
throw new Error("a-share sector evidence is stale for the market date");
|
|
15
|
-
}
|
|
16
|
-
return Object.freeze({ asOf, laggards, leaders, source: SOURCE });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function readSectorRanking(descending) {
|
|
20
|
-
const url = new URL("https://push2delay.eastmoney.com/api/qt/clist/get");
|
|
21
|
-
url.searchParams.set("pn", "1");
|
|
22
|
-
url.searchParams.set("pz", "3");
|
|
23
|
-
url.searchParams.set("po", descending ? "1" : "0");
|
|
24
|
-
url.searchParams.set("np", "1");
|
|
25
|
-
url.searchParams.set("fltt", "2");
|
|
26
|
-
url.searchParams.set("invt", "2");
|
|
27
|
-
url.searchParams.set("fid", "f3");
|
|
28
|
-
url.searchParams.set("fs", "m:90+t:2");
|
|
29
|
-
url.searchParams.set("fields", "f12,f14,f3,f124");
|
|
30
|
-
const body = await readAShareProviderResponse({
|
|
31
|
-
accept: "application/json",
|
|
32
|
-
maxBytes: 64 * 1024,
|
|
33
|
-
referer: "https://quote.eastmoney.com/",
|
|
34
|
-
source: "a-share sector evidence",
|
|
35
|
-
url
|
|
36
|
-
});
|
|
37
|
-
return parseSectorRanking(body.toString("utf8"));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseSectorRanking(body) {
|
|
41
|
-
let payload;
|
|
42
|
-
try {
|
|
43
|
-
payload = JSON.parse(body);
|
|
44
|
-
} catch (cause) {
|
|
45
|
-
throw new Error("a-share sector evidence is not valid JSON", { cause });
|
|
46
|
-
}
|
|
47
|
-
const rows = payload?.rc === 0 && Array.isArray(payload?.data?.diff) ? payload.data.diff : undefined;
|
|
48
|
-
if (!rows || rows.length !== 3) throw new Error("a-share sector evidence must contain three ranked industries");
|
|
49
|
-
return Object.freeze(
|
|
50
|
-
rows.map((row) => {
|
|
51
|
-
if (
|
|
52
|
-
typeof row?.f12 !== "string" ||
|
|
53
|
-
!row.f12 ||
|
|
54
|
-
typeof row?.f14 !== "string" ||
|
|
55
|
-
!row.f14 ||
|
|
56
|
-
!Number.isFinite(row?.f3) ||
|
|
57
|
-
!Number.isSafeInteger(row?.f124) ||
|
|
58
|
-
row.f124 <= 0
|
|
59
|
-
) {
|
|
60
|
-
throw new Error("a-share sector evidence contains an invalid industry record");
|
|
61
|
-
}
|
|
62
|
-
return Object.freeze({
|
|
63
|
-
asOf: new Date(row.f124 * 1000).toISOString(),
|
|
64
|
-
changePercent: row.f3,
|
|
65
|
-
id: row.f12,
|
|
66
|
-
name: row.f14
|
|
67
|
-
});
|
|
68
|
-
})
|
|
69
|
-
);
|
|
70
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
createAcpAgentServer,
|
|
5
|
-
createAcpPermissionBridge,
|
|
6
|
-
createAcpStdioAgentLoop,
|
|
7
|
-
createJsonAcpSessionStore,
|
|
8
|
-
serveAcpAgentOnStdio
|
|
9
|
-
} from "@rivus/agent/acp";
|
|
10
|
-
|
|
11
|
-
const command = process.env.RIVUS_ACP_SERVER_COMMAND?.trim();
|
|
12
|
-
if (!command) throw new Error("RIVUS_ACP_SERVER_COMMAND is required and must resolve to an explicit executable");
|
|
13
|
-
|
|
14
|
-
const workingDirectory = process.env.RIVUS_ACP_WORKING_DIRECTORY?.trim() || process.cwd();
|
|
15
|
-
const sessionStorePath = process.env.RIVUS_ACP_SESSION_STORE?.trim();
|
|
16
|
-
const permissionBridge = createAcpPermissionBridge();
|
|
17
|
-
const downstream = createAcpStdioAgentLoop({
|
|
18
|
-
arguments: parseArguments(process.env.RIVUS_ACP_SERVER_ARGUMENTS),
|
|
19
|
-
command,
|
|
20
|
-
environment: selectEnvironment(process.env.RIVUS_ACP_SERVER_ENV_KEYS),
|
|
21
|
-
onStderr: (text) => process.stderr.write(text),
|
|
22
|
-
permissionPolicy: permissionBridge.policy,
|
|
23
|
-
...(sessionStorePath ? { sessionStore: createJsonAcpSessionStore({ filePath: sessionStorePath }) } : {}),
|
|
24
|
-
workingDirectory
|
|
25
|
-
});
|
|
26
|
-
const server = createAcpAgentServer({
|
|
27
|
-
agentName: "rivus-acp-proxy",
|
|
28
|
-
loop: downstream.loop,
|
|
29
|
-
permissionBridge,
|
|
30
|
-
workingDirectory
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
await serveAcpAgentOnStdio(server);
|
|
35
|
-
} finally {
|
|
36
|
-
await downstream.dispose();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function parseArguments(value) {
|
|
40
|
-
if (!value?.trim()) return [];
|
|
41
|
-
const parsed = JSON.parse(value);
|
|
42
|
-
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
|
|
43
|
-
throw new Error("RIVUS_ACP_SERVER_ARGUMENTS must be a JSON array of strings");
|
|
44
|
-
}
|
|
45
|
-
return parsed;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function selectEnvironment(value) {
|
|
49
|
-
const selected = {};
|
|
50
|
-
for (const key of value
|
|
51
|
-
?.split(",")
|
|
52
|
-
.map((item) => item.trim())
|
|
53
|
-
.filter(Boolean) ?? []) {
|
|
54
|
-
const environmentValue = process.env[key];
|
|
55
|
-
if (environmentValue !== undefined) selected[key] = environmentValue;
|
|
56
|
-
}
|
|
57
|
-
return selected;
|
|
58
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { readBoundedHttpsResponse } from "./https-response-reader.mjs";
|
|
2
|
-
|
|
3
|
-
export async function readCurrentWeather(input) {
|
|
4
|
-
if (input !== undefined && (typeof input !== "object" || input === null || Array.isArray(input))) {
|
|
5
|
-
throw new Error("current-weather input must be an object");
|
|
6
|
-
}
|
|
7
|
-
const requestedLocation = input?.location;
|
|
8
|
-
if (
|
|
9
|
-
requestedLocation !== undefined &&
|
|
10
|
-
(typeof requestedLocation !== "string" || !requestedLocation.trim() || requestedLocation.length > 100)
|
|
11
|
-
) {
|
|
12
|
-
throw new Error("current-weather location must be a non-empty string up to 100 characters");
|
|
13
|
-
}
|
|
14
|
-
const location = requestedLocation?.trim() || process.env.RIVUS_WEATHER_DEFAULT_LOCATION?.trim() || "北京";
|
|
15
|
-
const geocodingUrl = new URL("https://geocoding-api.open-meteo.com/v1/search");
|
|
16
|
-
geocodingUrl.search = new URLSearchParams({ count: "1", format: "json", language: "zh", name: location }).toString();
|
|
17
|
-
const geocoding = await fetchJson(geocodingUrl, "weather location lookup");
|
|
18
|
-
const place = Array.isArray(geocoding.results) ? geocoding.results[0] : undefined;
|
|
19
|
-
if (
|
|
20
|
-
!place ||
|
|
21
|
-
typeof place.name !== "string" ||
|
|
22
|
-
typeof place.latitude !== "number" ||
|
|
23
|
-
typeof place.longitude !== "number"
|
|
24
|
-
) {
|
|
25
|
-
throw new Error(`current-weather could not resolve location: ${location}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const forecastUrl = new URL("https://api.open-meteo.com/v1/forecast");
|
|
29
|
-
forecastUrl.search = new URLSearchParams({
|
|
30
|
-
current: "temperature_2m,apparent_temperature,weather_code,wind_speed_10m",
|
|
31
|
-
daily: "temperature_2m_max,temperature_2m_min,precipitation_probability_max",
|
|
32
|
-
forecast_days: "1",
|
|
33
|
-
latitude: String(place.latitude),
|
|
34
|
-
longitude: String(place.longitude),
|
|
35
|
-
timezone: "auto"
|
|
36
|
-
}).toString();
|
|
37
|
-
const forecast = await fetchJson(forecastUrl, "weather forecast lookup");
|
|
38
|
-
const current = forecast.current;
|
|
39
|
-
const daily = forecast.daily;
|
|
40
|
-
if (!current || typeof current !== "object" || !daily || typeof daily !== "object") {
|
|
41
|
-
throw new Error("current-weather provider returned an incomplete forecast");
|
|
42
|
-
}
|
|
43
|
-
const weatherCode = readNumber(current.weather_code, "weather_code");
|
|
44
|
-
return {
|
|
45
|
-
apparentTemperatureC: readNumber(current.apparent_temperature, "apparent_temperature"),
|
|
46
|
-
condition: describeWeatherCode(weatherCode),
|
|
47
|
-
date: readFirst(daily.time, "date"),
|
|
48
|
-
location: place.country ? `${place.name}, ${place.country}` : place.name,
|
|
49
|
-
observedAt: readString(current.time, "observed time"),
|
|
50
|
-
precipitationProbabilityPercent: readFirstNumber(daily.precipitation_probability_max, "precipitation probability"),
|
|
51
|
-
temperatureC: readNumber(current.temperature_2m, "temperature"),
|
|
52
|
-
temperatureMaxC: readFirstNumber(daily.temperature_2m_max, "maximum temperature"),
|
|
53
|
-
temperatureMinC: readFirstNumber(daily.temperature_2m_min, "minimum temperature"),
|
|
54
|
-
timezone: readString(forecast.timezone ?? place.timezone, "timezone"),
|
|
55
|
-
weatherCode,
|
|
56
|
-
windSpeedKmh: readNumber(current.wind_speed_10m, "wind speed")
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function fetchJson(url, operation) {
|
|
61
|
-
let response;
|
|
62
|
-
try {
|
|
63
|
-
response = await readBoundedHttpsResponse(url, {
|
|
64
|
-
headers: { accept: "application/json" },
|
|
65
|
-
maxBytes: 1024 * 1024
|
|
66
|
-
});
|
|
67
|
-
} catch (error) {
|
|
68
|
-
throw new Error(`${operation} failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
69
|
-
cause: error
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
if (response.status < 200 || response.status >= 300) {
|
|
73
|
-
throw new Error(`${operation} failed with HTTP ${response.status}`);
|
|
74
|
-
}
|
|
75
|
-
let value;
|
|
76
|
-
try {
|
|
77
|
-
value = JSON.parse(response.body.toString("utf8"));
|
|
78
|
-
} catch {
|
|
79
|
-
throw new Error(`${operation} returned invalid JSON`);
|
|
80
|
-
}
|
|
81
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
82
|
-
throw new Error(`${operation} returned invalid JSON`);
|
|
83
|
-
}
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function readNumber(value, field) {
|
|
88
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`current-weather missing ${field}`);
|
|
89
|
-
return value;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function readString(value, field) {
|
|
93
|
-
if (typeof value !== "string" || !value) throw new Error(`current-weather missing ${field}`);
|
|
94
|
-
return value;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function readFirst(value, field) {
|
|
98
|
-
return readString(Array.isArray(value) ? value[0] : undefined, field);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function readFirstNumber(value, field) {
|
|
102
|
-
return readNumber(Array.isArray(value) ? value[0] : undefined, field);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function describeWeatherCode(code) {
|
|
106
|
-
if (code === 0) return "晴";
|
|
107
|
-
if (code === 1 || code === 2) return "晴间多云";
|
|
108
|
-
if (code === 3) return "阴";
|
|
109
|
-
if (code === 45 || code === 48) return "有雾";
|
|
110
|
-
if (code >= 51 && code <= 57) return "毛毛雨";
|
|
111
|
-
if (code >= 61 && code <= 67) return "雨";
|
|
112
|
-
if (code >= 71 && code <= 77) return "雪";
|
|
113
|
-
if (code >= 80 && code <= 82) return "阵雨";
|
|
114
|
-
if (code >= 85 && code <= 86) return "阵雪";
|
|
115
|
-
if (code >= 95 && code <= 99) return "雷暴";
|
|
116
|
-
return "未知";
|
|
117
|
-
}
|