@rivus/agent 0.1.1 → 0.4.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/README.md +19 -4
- package/dist/acp.d.ts +98 -0
- package/dist/acp.js +436 -0
- package/dist/agent-loop.d.ts +420 -0
- package/dist/agent-loop.js +118 -0
- package/dist/agent-memory.d.ts +98 -0
- package/dist/cli.js +508 -5
- package/dist/index.d.ts +15 -427
- package/dist/index.js +131 -180
- package/dist/rivus-daemon-cli.js +558 -527
- package/dist/rivus-plugin-testkit.d.ts +3 -98
- package/dist/rivus-plugin-testkit.js +2 -2
- package/examples/a-share-briefing-analysis.mjs +93 -0
- package/examples/a-share-briefing-renderer.mjs +257 -0
- package/examples/a-share-index-evidence.mjs +99 -0
- package/examples/a-share-market-briefing.mjs +83 -0
- package/examples/a-share-market-date.mjs +10 -0
- package/examples/a-share-overseas-evidence.mjs +86 -0
- package/examples/a-share-policy-evidence.mjs +145 -0
- package/examples/a-share-provider-response.mjs +21 -0
- package/examples/a-share-sector-evidence.mjs +70 -0
- package/examples/acp-stdio-proxy.mjs +55 -0
- package/examples/current-weather.mjs +6 -32
- package/examples/https-response-reader.mjs +36 -0
- package/examples/pi-feishu-deployment.bootstrap.ts +2 -2
- package/examples/rivus-agents.plugin.mjs +55 -3
- package/examples/rivus-starter.plugin.mjs +45 -0
- package/examples/rivus.config.json +30 -1
- package/package.json +17 -7
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createAcpAgentServer,
|
|
5
|
+
createAcpPermissionBridge,
|
|
6
|
+
createAcpStdioAgentLoop,
|
|
7
|
+
serveAcpAgentOnStdio
|
|
8
|
+
} from "@rivus/agent/acp";
|
|
9
|
+
|
|
10
|
+
const command = process.env.RIVUS_ACP_SERVER_COMMAND?.trim();
|
|
11
|
+
if (!command) throw new Error("RIVUS_ACP_SERVER_COMMAND is required and must resolve to an explicit executable");
|
|
12
|
+
|
|
13
|
+
const workingDirectory = process.env.RIVUS_ACP_WORKING_DIRECTORY?.trim() || process.cwd();
|
|
14
|
+
const permissionBridge = createAcpPermissionBridge();
|
|
15
|
+
const downstream = createAcpStdioAgentLoop({
|
|
16
|
+
arguments: parseArguments(process.env.RIVUS_ACP_SERVER_ARGUMENTS),
|
|
17
|
+
command,
|
|
18
|
+
environment: selectEnvironment(process.env.RIVUS_ACP_SERVER_ENV_KEYS),
|
|
19
|
+
onStderr: (text) => process.stderr.write(text),
|
|
20
|
+
permissionPolicy: permissionBridge.policy,
|
|
21
|
+
workingDirectory
|
|
22
|
+
});
|
|
23
|
+
const server = createAcpAgentServer({
|
|
24
|
+
agentName: "rivus-acp-proxy",
|
|
25
|
+
loop: downstream.loop,
|
|
26
|
+
permissionBridge,
|
|
27
|
+
workingDirectory
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await serveAcpAgentOnStdio(server);
|
|
32
|
+
} finally {
|
|
33
|
+
await downstream.dispose();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseArguments(value) {
|
|
37
|
+
if (!value?.trim()) return [];
|
|
38
|
+
const parsed = JSON.parse(value);
|
|
39
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
|
|
40
|
+
throw new Error("RIVUS_ACP_SERVER_ARGUMENTS must be a JSON array of strings");
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function selectEnvironment(value) {
|
|
46
|
+
const selected = {};
|
|
47
|
+
for (const key of value
|
|
48
|
+
?.split(",")
|
|
49
|
+
.map((item) => item.trim())
|
|
50
|
+
.filter(Boolean) ?? []) {
|
|
51
|
+
const environmentValue = process.env[key];
|
|
52
|
+
if (environmentValue !== undefined) selected[key] = environmentValue;
|
|
53
|
+
}
|
|
54
|
+
return selected;
|
|
55
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readBoundedHttpsResponse } from "./https-response-reader.mjs";
|
|
2
2
|
|
|
3
3
|
export async function readCurrentWeather(input) {
|
|
4
4
|
if (input !== undefined && (typeof input !== "object" || input === null || Array.isArray(input))) {
|
|
@@ -60,7 +60,10 @@ export async function readCurrentWeather(input) {
|
|
|
60
60
|
async function fetchJson(url, operation) {
|
|
61
61
|
let response;
|
|
62
62
|
try {
|
|
63
|
-
response = await
|
|
63
|
+
response = await readBoundedHttpsResponse(url, {
|
|
64
|
+
headers: { accept: "application/json" },
|
|
65
|
+
maxBytes: 1024 * 1024
|
|
66
|
+
});
|
|
64
67
|
} catch (error) {
|
|
65
68
|
throw new Error(`${operation} failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
66
69
|
cause: error
|
|
@@ -71,7 +74,7 @@ async function fetchJson(url, operation) {
|
|
|
71
74
|
}
|
|
72
75
|
let value;
|
|
73
76
|
try {
|
|
74
|
-
value = JSON.parse(response.body);
|
|
77
|
+
value = JSON.parse(response.body.toString("utf8"));
|
|
75
78
|
} catch {
|
|
76
79
|
throw new Error(`${operation} returned invalid JSON`);
|
|
77
80
|
}
|
|
@@ -81,35 +84,6 @@ async function fetchJson(url, operation) {
|
|
|
81
84
|
return value;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
function requestJson(url) {
|
|
85
|
-
return new Promise((resolve, reject) => {
|
|
86
|
-
const request = get(
|
|
87
|
-
url,
|
|
88
|
-
{
|
|
89
|
-
family: 4,
|
|
90
|
-
headers: { accept: "application/json" },
|
|
91
|
-
signal: AbortSignal.timeout(10_000)
|
|
92
|
-
},
|
|
93
|
-
(response) => {
|
|
94
|
-
const status = response.statusCode ?? 0;
|
|
95
|
-
response.once("error", reject);
|
|
96
|
-
if (status < 200 || status >= 300) {
|
|
97
|
-
response.resume();
|
|
98
|
-
resolve({ body: "", status });
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
let body = "";
|
|
102
|
-
response.setEncoding("utf8");
|
|
103
|
-
response.on("data", (chunk) => {
|
|
104
|
-
body += chunk;
|
|
105
|
-
});
|
|
106
|
-
response.once("end", () => resolve({ body, status }));
|
|
107
|
-
}
|
|
108
|
-
);
|
|
109
|
-
request.once("error", reject);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
87
|
function readNumber(value, field) {
|
|
114
88
|
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`current-weather missing ${field}`);
|
|
115
89
|
return value;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { get } from "node:https";
|
|
2
|
+
|
|
3
|
+
export function readBoundedHttpsResponse(url, options) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const request = get(
|
|
6
|
+
url,
|
|
7
|
+
{
|
|
8
|
+
family: 4,
|
|
9
|
+
headers: options.headers,
|
|
10
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 10_000)
|
|
11
|
+
},
|
|
12
|
+
(response) => {
|
|
13
|
+
const status = response.statusCode ?? 0;
|
|
14
|
+
response.once("error", reject);
|
|
15
|
+
if (status < 200 || status >= 300) {
|
|
16
|
+
response.resume();
|
|
17
|
+
resolve({ body: Buffer.alloc(0), status });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let length = 0;
|
|
22
|
+
response.on("data", (chunk) => {
|
|
23
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
24
|
+
length += buffer.length;
|
|
25
|
+
if (length > options.maxBytes) {
|
|
26
|
+
request.destroy(new Error(`HTTPS response exceeds ${options.maxBytes} bytes`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
chunks.push(buffer);
|
|
30
|
+
});
|
|
31
|
+
response.once("end", () => resolve({ body: Buffer.concat(chunks), status }));
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
request.once("error", reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
createAgentHarness,
|
|
16
16
|
createAgentHarnessPooledRuntime,
|
|
17
17
|
createRivusMemoryToolDescriptor,
|
|
18
|
-
|
|
18
|
+
createConfiguredFeishuAutomationCardSender,
|
|
19
19
|
createConfiguredFeishuCardKitPublisher,
|
|
20
20
|
createConfiguredFeishuCardKitTargetPreparation,
|
|
21
21
|
createConfiguredFeishuHumanInteractionPresenter,
|
|
@@ -113,7 +113,7 @@ export async function createRivusDeploymentAdapters(context: RivusDeploymentBoot
|
|
|
113
113
|
pi: {}
|
|
114
114
|
};
|
|
115
115
|
const openApiClient = createOpenApiClient(config);
|
|
116
|
-
const sender =
|
|
116
|
+
const sender = createConfiguredFeishuAutomationCardSender({
|
|
117
117
|
client: openApiClient,
|
|
118
118
|
config
|
|
119
119
|
});
|
|
@@ -3,21 +3,31 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { readCurrentWeather } from "./current-weather.mjs";
|
|
6
|
+
import { readAShareMarketBriefing } from "./a-share-market-briefing.mjs";
|
|
6
7
|
import { createHtmlArtifactWriter, createLarkDriveHtmlUploader } from "./html-drive-tools.mjs";
|
|
7
8
|
|
|
8
9
|
const TOOL_IDS = {
|
|
10
|
+
aShareMarketBriefing: "rivus-example-agents/a-share-market-briefing",
|
|
9
11
|
currentWeather: "rivus-example-agents/current-weather",
|
|
10
12
|
larkDriveUploadHtml: "rivus-example-agents/lark-drive-upload-html",
|
|
11
13
|
runtimeInfo: "rivus-example-agents/runtime-info",
|
|
12
14
|
saveNote: "rivus-example-agents/save-note",
|
|
13
15
|
writeHtmlArtifact: "rivus-example-agents/write-html-artifact"
|
|
14
16
|
};
|
|
15
|
-
const
|
|
17
|
+
const AGENT_A_TOOL_IDS = [
|
|
18
|
+
TOOL_IDS.runtimeInfo,
|
|
19
|
+
TOOL_IDS.saveNote,
|
|
20
|
+
TOOL_IDS.currentWeather,
|
|
21
|
+
TOOL_IDS.aShareMarketBriefing
|
|
22
|
+
];
|
|
23
|
+
const AGENT_B_TOOL_IDS = [TOOL_IDS.runtimeInfo, TOOL_IDS.saveNote, TOOL_IDS.currentWeather];
|
|
16
24
|
const WEATHER_INSTRUCTION =
|
|
17
25
|
"遇到当前或今日天气问题时必须调用 current-weather;用户未指定地点时不要猜测,省略 location 并在回答中明确说明工具返回的默认地点。";
|
|
18
26
|
const MEMORY_INSTRUCTION =
|
|
19
27
|
"用户明确要求记住、回忆或遗忘偏好时,必须调用 rivus_memory;propose 会持久化一条可在同 Scope 后续检索的待确认候选,不得声称用户已经确认,也不得声称候选尚未写入。";
|
|
20
28
|
const DAILY_WORD_AUTOMATION_ID = "rivus-example-agents/daily-ielts-word";
|
|
29
|
+
const A_SHARE_PRE_MARKET_AUTOMATION_ID = "rivus-example-agents/a-share-pre-market";
|
|
30
|
+
const A_SHARE_POST_MARKET_AUTOMATION_ID = "rivus-example-agents/a-share-post-market";
|
|
21
31
|
const LANGFUSE_PUBLISHER_SKILL_ID = "rivus-example-agents/langfuse-html-publisher";
|
|
22
32
|
const LANGFUSE_PUBLISHER_SKILL = Object.freeze({
|
|
23
33
|
content: [
|
|
@@ -101,6 +111,25 @@ export default {
|
|
|
101
111
|
risk: "observe",
|
|
102
112
|
version: "1.0.0"
|
|
103
113
|
});
|
|
114
|
+
registry.registerTool({
|
|
115
|
+
createExecutor: () => ({ execute: readAShareMarketBriefing }),
|
|
116
|
+
description:
|
|
117
|
+
"Read a bounded, evidence-backed A-share pre-market or post-market analysis briefing with attributed facts, labeled inferences, source time, and stale-data handling",
|
|
118
|
+
digest: "sha256:example-a-share-market-briefing-v3",
|
|
119
|
+
id: TOOL_IDS.aShareMarketBriefing,
|
|
120
|
+
idempotency: "none",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
properties: {
|
|
124
|
+
occurrence: { format: "date-time", type: "string" },
|
|
125
|
+
session: { enum: ["pre-market", "post-market"], type: "string" }
|
|
126
|
+
},
|
|
127
|
+
required: ["session", "occurrence"],
|
|
128
|
+
type: "object"
|
|
129
|
+
},
|
|
130
|
+
risk: "observe",
|
|
131
|
+
version: "1.1.0"
|
|
132
|
+
});
|
|
104
133
|
registry.registerTool({
|
|
105
134
|
createExecutor: () => createHtmlArtifactWriter(),
|
|
106
135
|
description:
|
|
@@ -161,7 +190,7 @@ export default {
|
|
|
161
190
|
model: {},
|
|
162
191
|
skills: { allow: [] },
|
|
163
192
|
systemPrompt: `You are Rivus Agent A. Be concise, analytical, and explicit about evidence. ${WEATHER_INSTRUCTION} ${MEMORY_INSTRUCTION}`,
|
|
164
|
-
tools: { allow:
|
|
193
|
+
tools: { allow: AGENT_A_TOOL_IDS }
|
|
165
194
|
});
|
|
166
195
|
registry.registerAgentProfile({
|
|
167
196
|
displayName: "Rivus Agent B",
|
|
@@ -170,7 +199,7 @@ export default {
|
|
|
170
199
|
model: {},
|
|
171
200
|
skills: { allow: [] },
|
|
172
201
|
systemPrompt: `You are Rivus Agent B. Focus on independent verification and clearly state uncertainty. ${WEATHER_INSTRUCTION} ${MEMORY_INSTRUCTION}`,
|
|
173
|
-
tools: { allow:
|
|
202
|
+
tools: { allow: AGENT_B_TOOL_IDS }
|
|
174
203
|
});
|
|
175
204
|
registry.registerAgentProfile({
|
|
176
205
|
displayName: "Rivus Langfuse publishing demo",
|
|
@@ -200,9 +229,32 @@ export default {
|
|
|
200
229
|
requestedSkillIds: [],
|
|
201
230
|
requestedToolIds: []
|
|
202
231
|
});
|
|
232
|
+
registry.registerAutomation(createAShareAutomation(A_SHARE_PRE_MARKET_AUTOMATION_ID, "pre-market"));
|
|
233
|
+
registry.registerAutomation(createAShareAutomation(A_SHARE_POST_MARKET_AUTOMATION_ID, "post-market"));
|
|
203
234
|
}
|
|
204
235
|
};
|
|
205
236
|
|
|
237
|
+
function createAShareAutomation(id, session) {
|
|
238
|
+
const label = session === "pre-market" ? "盘前" : "盘后";
|
|
239
|
+
return {
|
|
240
|
+
createInput: ({ occurrence }) => {
|
|
241
|
+
const parameters = JSON.stringify({ session, occurrence });
|
|
242
|
+
return {
|
|
243
|
+
text: [
|
|
244
|
+
`计划发生时间:${occurrence}。生成 A 股${label}简报。`,
|
|
245
|
+
`必须且只调用一次 ${TOOL_IDS.aShareMarketBriefing},参数严格使用:${parameters}。`,
|
|
246
|
+
"工具返回的 markdown 字段已区分市场事实和分析推断并列出来源,就是最终交付正文;必须原样输出,不增加开场、解释、预测、荐股、因果归因或任何工具结果之外的数字。",
|
|
247
|
+
"如果工具失败,不得估算或补全行情,让本次 Automation 明确失败以便按同一 Tick 重试。"
|
|
248
|
+
].join("\n")
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
id,
|
|
252
|
+
profileId: "agent-a",
|
|
253
|
+
requestedSkillIds: [],
|
|
254
|
+
requestedToolIds: [TOOL_IDS.aShareMarketBriefing]
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
206
258
|
async function saveNote(input, context) {
|
|
207
259
|
if (
|
|
208
260
|
!input ||
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readCurrentWeather } from "./current-weather.mjs";
|
|
2
|
+
|
|
3
|
+
const TOOL_ID = "rivus-starter/current-weather";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
manifest: {
|
|
7
|
+
apiVersion: "1",
|
|
8
|
+
id: "rivus-starter",
|
|
9
|
+
version: "1.0.0"
|
|
10
|
+
},
|
|
11
|
+
register(registry) {
|
|
12
|
+
registry.registerTool({
|
|
13
|
+
createExecutor: () => ({ execute: readCurrentWeather }),
|
|
14
|
+
description:
|
|
15
|
+
"Read current conditions and today's forecast from Open-Meteo, using the configured default when location is omitted",
|
|
16
|
+
digest: "sha256:rivus-starter-current-weather-v1",
|
|
17
|
+
id: TOOL_ID,
|
|
18
|
+
idempotency: "none",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {
|
|
22
|
+
location: {
|
|
23
|
+
description: "City or place name. Omit it when the user did not specify one.",
|
|
24
|
+
maxLength: 100,
|
|
25
|
+
minLength: 1,
|
|
26
|
+
type: "string"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
type: "object"
|
|
30
|
+
},
|
|
31
|
+
risk: "observe",
|
|
32
|
+
version: "1.0.0"
|
|
33
|
+
});
|
|
34
|
+
registry.registerAgentProfile({
|
|
35
|
+
displayName: "Rivus Agent",
|
|
36
|
+
id: "agent-a",
|
|
37
|
+
memory: { scopes: [] },
|
|
38
|
+
model: {},
|
|
39
|
+
skills: { allow: [] },
|
|
40
|
+
systemPrompt:
|
|
41
|
+
"You are a concise local assistant. For current or today's weather, call current-weather. If the user did not name a location, omit location and report the Tool's resolved place.",
|
|
42
|
+
tools: { allow: [TOOL_ID] }
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|