114 lines
4.5 KiB
TypeScript
114 lines
4.5 KiB
TypeScript
import {
|
||
accountName,
|
||
downloadStatusName,
|
||
healthName,
|
||
humanNow,
|
||
humanRelative,
|
||
kindName,
|
||
subscriptionStatusName,
|
||
} from "../format.ts";
|
||
import type { EnvironmentFacts, PromptSection } from "../types.ts";
|
||
|
||
// 第 2 层:环境事实。会话开始时服务端从业务表读出的摘要,让模型知道"有什么"再去查"怎么样":
|
||
// 反例(docs/design/agent-report-sample-bad.md)里模型只看了站点列表,就汇报"系统里没有下载器"。
|
||
// 只写名称、类型、状态和计数,不写地址、ID 与凭证;数字以会话开始时为准,细节仍以工具结果为准。
|
||
|
||
const MAX_NAMED = 6;
|
||
|
||
function names<T>(items: readonly T[], render: (x: T) => string): string {
|
||
const shown = items.slice(0, MAX_NAMED).map(render);
|
||
const more = items.length - shown.length;
|
||
return more > 0 ? `${shown.join(";")};另有 ${more} 个` : shown.join(";");
|
||
}
|
||
|
||
/** 计数表 → "追更中 3、已取消 2";全 0 返回空串 */
|
||
function counts(m: Record<string, number>, label: (k: string) => string): string {
|
||
return Object.entries(m)
|
||
.filter(([, n]) => n > 0)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.map(([k, n]) => `${label(k)} ${n}`)
|
||
.join("、");
|
||
}
|
||
|
||
const total = (m: Record<string, number>) => Object.values(m).reduce((a, b) => a + b, 0);
|
||
|
||
export function renderEnvironment(f: EnvironmentFacts): string[] {
|
||
const now = new Date(f.now);
|
||
const tz = f.timeZone;
|
||
const rel = (iso: string | null) => humanRelative(iso, now, tz);
|
||
const lines: string[] = [`现在:${humanNow(now, tz)}。`];
|
||
|
||
const dl = f.downloaders.filter((d) => d.enabled);
|
||
if (dl.length === 0)
|
||
lines.push(
|
||
f.downloaders.length > 0
|
||
? `下载器:${f.downloaders.length} 个都已停用。`
|
||
: "下载器:还没有接入(设置 → 下载器)。",
|
||
);
|
||
else
|
||
lines.push(
|
||
`下载器 ${dl.length} 个:${names(dl, (d) => {
|
||
const seen = rel(d.lastSeenAt);
|
||
const t = d.tasks;
|
||
const tasks =
|
||
t.total > 0
|
||
? `,本工具管理 ${t.total} 个任务(进行中 ${t.active}、失败 ${t.failed})`
|
||
: ",没有本工具管理的任务";
|
||
return `「${d.name}」${kindName(d.kind)},${healthName(d.health)}${seen ? `(${seen})` : ""}${tasks}`;
|
||
})}。`,
|
||
);
|
||
|
||
const ms = f.mediaServers.filter((m) => m.enabled);
|
||
lines.push(
|
||
ms.length === 0
|
||
? "媒体服务器:还没有接入。"
|
||
: `媒体服务器 ${ms.length} 个:${names(ms, (m) => {
|
||
const seen = rel(m.lastSeenAt);
|
||
return `「${m.name}」${kindName(m.kind)},${healthName(m.health)}${seen ? `(${seen})` : ""}`;
|
||
})}。`,
|
||
);
|
||
|
||
const sites = f.sites.filter((s) => s.enabled);
|
||
lines.push(
|
||
sites.length === 0
|
||
? "站点:还没有启用的站点。"
|
||
: `站点 ${sites.length} 个:${names(sites, (s) => {
|
||
const at = rel(s.lastHealthAt);
|
||
const health =
|
||
s.health === "unknown" ? "还没检查过" : `上次检查${healthName(s.health)}${at ? `(${at})` : ""}`;
|
||
return `「${s.name}」${kindName(s.kind)},${health}${s.account ? `,${accountName(s.account)}` : ""}`;
|
||
})}。`,
|
||
);
|
||
|
||
const roots = f.libraryRoots.filter((r) => r.enabled);
|
||
lines.push(
|
||
roots.length === 0
|
||
? "媒体库目录:还没有登记。"
|
||
: `媒体库目录 ${roots.length} 个:${names(roots, (r) => `「${r.name}」${healthName(r.health)}`)}。`,
|
||
);
|
||
|
||
const subs = total(f.subscriptions);
|
||
lines.push(subs === 0 ? "订阅:没有。" : `订阅 ${subs} 个:${counts(f.subscriptions, subscriptionStatusName)}。`);
|
||
const dls = total(f.downloads);
|
||
lines.push(
|
||
dls === 0
|
||
? "下载任务(本工具管理):没有。"
|
||
: `下载任务(本工具管理)${dls} 个:${counts(f.downloads, downloadStatusName)}。`,
|
||
);
|
||
lines.push(f.openDecisions === 0 ? "待处理事项:没有。" : `待处理事项:${f.openDecisions} 条未关闭。`);
|
||
if (f.webSearch !== undefined) lines.push(f.webSearch ? "网页搜索:已开启。" : "网页搜索:未开启。");
|
||
return lines;
|
||
}
|
||
|
||
export function environmentLayer(f: EnvironmentFacts | null | undefined): PromptSection {
|
||
if (!f) return { layer: "environment", title: "当前环境", lines: [] };
|
||
return {
|
||
layer: "environment",
|
||
title: "当前环境",
|
||
lines: [
|
||
"会话开始时的快照,只有名称、状态和计数;要细节或最新状态用下面的能力查。这里写了「还没有接入」的就是真的没有,不用再向用户确认。",
|
||
...renderEnvironment(f).map((l) => `- ${l}`),
|
||
],
|
||
};
|
||
}
|