diff --git a/README.md b/README.md index 4649a0e..1f85c3e 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ pi remove npm:pi-ntfy ```json { "enabled": true, - "modes": ["tui"], + "excludedModes": [], "titleTemplate": "[{{project}}] {{status}} · {{session}}", "template": "项目: {{project}}\\n会话: {{session}}\\n任务: {{task}}\\n状态: {{status}}\\n原因: {{reason}}\\n摘要: {{summary}}\\n耗时: {{duration}}\\n模型: {{model}}\\n主机: {{host}}\\n时间: {{date}} {{time}}\\n工具: {{tools}}", "channels": [ @@ -57,6 +57,8 @@ pi remove npm:pi-ntfy `token` 可以使用 `$NTFY_TOKEN` 或 `${NTFY_TOKEN}` 引用环境变量;配置文件路径本身不受环境变量影响。 +工作模式:默认支持 pi 的全部工作模式(`tui`、`json`、`rpc`、`print`),配置里无需列出支持的模式;`excludedModes` 仅列出**不**推送的模式,例如 `"excludedModes": ["json"]` 可让 `pi --mode json -p` 子代理保持安静。旧配置中的 `modes` 白名单已废弃,会被忽略并给出提示。 + 配置字段: ```json diff --git a/command.ts b/command.ts index ca57387..3d2ac94 100644 --- a/command.ts +++ b/command.ts @@ -116,7 +116,7 @@ function describeConfig( `配置: ${config.path}`, `渠道: ${channel ? formatChannelLabel(channel, 0) : "ntfy(未配置)"}`, `事件: ${(["idle", "interrupted", "needInput", "exit"] as const).map((key) => `${key}=${config.events[key] ? "on" : "off"}`).join(" ")}`, - `模式: ${config.modes.join(",")}`, + `模式: 全部${config.excludedModes.length > 0 ? `(排除: ${config.excludedModes.join(", ")})` : ""}`, ]; if (last && describeResults) lines.push( diff --git a/config.example.json b/config.example.json index 1efb515..d106411 100644 --- a/config.example.json +++ b/config.example.json @@ -1,8 +1,8 @@ { - "_说明": "pi-ntfy 只支持 NTFY。复制为 config.json 后按需修改。", + "_说明": "pi-ntfy 只支持 NTFY。默认所有工作模式(tui/json/rpc/print)都会推送;excludedModes 仅列出不需要推送的模式,例如 [\"json\"] 可让 pi --mode json -p 的子代理保持安静。复制为 config.json 后按需修改。", "_占位符": "{{status}} {{reason}} {{text}} {{title}} {{event}} {{cwd}} {{project}} {{duration}} {{session}} {{sessionId}} {{model}} {{mode}} {{thinking}} {{task}} {{summary}} {{tools}} {{host}} {{time}} {{date}}", "enabled": true, - "modes": ["tui"], + "excludedModes": [], "timeoutMs": 5000, "shutdownTimeoutMs": 2000, "dedupeMs": 3000, diff --git a/config.ts b/config.ts index 296a070..948ba0e 100644 --- a/config.ts +++ b/config.ts @@ -9,7 +9,7 @@ import { } from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; -import type { ChannelConfig, NotifyConfig, NtfyChannel } from "./types.ts"; +import { ALL_MODES, type ChannelConfig, type NotifyConfig, type NtfyChannel } from "./types.ts"; const CONFIG_PATH = join(homedir(), ".pi", "agent", "pi-ntfy.json"); @@ -75,20 +75,44 @@ function str(value: unknown, fallback: string): string { return typeof value === "string" && value.length > 0 ? value : fallback; } -function strArray(value: unknown, fallback: string[]): string[] { - if (!Array.isArray(value)) return fallback; - const items = value.filter( +function strArray(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value.filter( (v): v is string => typeof v === "string" && v.length > 0, ); - return items.length > 0 ? items : fallback; +} + +/** Validate excludedModes: known modes only, deduped, with warnings. */ +function excludedModes( + value: unknown, + fallback: string[], + warnings: string[], +): string[] { + if (value === undefined) return fallback; + if (!Array.isArray(value)) { + warnings.push('"excludedModes" 必须是数组,已忽略'); + return []; + } + const out: string[] = []; + for (const item of value) { + if (typeof item !== "string" || item.length === 0) continue; + if (!(ALL_MODES as readonly string[]).includes(item)) { + warnings.push( + `excludedModes 中的 "${item}" 不是有效模式(可选: ${ALL_MODES.join("/")}),已忽略`, + ); + continue; + } + if (!out.includes(item)) out.push(item); + } + return out; } export function defaultConfig(path: string): NotifyConfig { return { enabled: true, - // Only the interactive TUI by default: pi subagents run `pi --mode json -p`, - // and pushing for every one of those would be pure noise. - modes: ["tui"], + // All working modes (tui/json/rpc/print) push by default. To quiet + // `pi --mode json -p` subagents, set "excludedModes": ["json"]. + excludedModes: [], timeoutMs: 5000, shutdownTimeoutMs: 2000, dedupeMs: 3000, @@ -168,7 +192,15 @@ export function loadConfig(): NotifyConfig { } config.enabled = bool(parsed.enabled, config.enabled); - config.modes = strArray(parsed.modes, config.modes); + config.excludedModes = excludedModes( + parsed.excludedModes, + config.excludedModes, + config.warnings, + ); + if (parsed.modes !== undefined) + config.warnings.push( + '"modes" 已废弃,请改用 "excludedModes"(仅列出不需要推送的模式)', + ); config.timeoutMs = num(parsed.timeoutMs, config.timeoutMs, 200, 60_000); config.shutdownTimeoutMs = num( parsed.shutdownTimeoutMs, @@ -186,7 +218,7 @@ export function loadConfig(): NotifyConfig { config.maxTextChars = num(parsed.maxTextChars, config.maxTextChars, 20, 4000); config.titleTemplate = str(parsed.titleTemplate, config.titleTemplate); config.template = str(parsed.template, config.template); - config.needInputTools = strArray(parsed.needInputTools, config.needInputTools); + config.needInputTools = strArray(parsed.needInputTools); config.debug = bool(parsed.debug, config.debug); if (parsed.events && typeof parsed.events === "object") { @@ -240,7 +272,7 @@ export const CHANNEL_SETTABLE_FIELDS: Record = { export interface RawConfigFile { enabled?: boolean; - modes?: string[]; + excludedModes?: string[]; timeoutMs?: number; shutdownTimeoutMs?: number; dedupeMs?: number; @@ -268,8 +300,12 @@ export function loadRawConfig(): { exists: false, data: { enabled: true, - modes: ["tui"], - events: { idle: true, interrupted: true, needInput: true, exit: false }, + events: { + idle: true, + interrupted: true, + needInput: true, + exit: false, + }, channels: [], }, }; diff --git a/index.ts b/index.ts index f871292..fe26c57 100644 --- a/index.ts +++ b/index.ts @@ -74,8 +74,8 @@ export default function piNtfy(pi: ExtensionAPI) { if (!runtimeEnabled) return undefined; const config = loadConfig(); if (!config.enabled || !config.exists) return undefined; - // Subagents run `pi --mode json -p`; pushing for each of those is noise. - if (!config.modes.includes(mode)) return undefined; + // All modes push unless listed in excludedModes (e.g. subagents on json). + if (config.excludedModes.includes(mode)) return undefined; return config; } diff --git a/types.ts b/types.ts index af54215..8e5bc26 100644 --- a/types.ts +++ b/types.ts @@ -34,7 +34,8 @@ export type ChannelConfig = NtfyChannel; export interface NotifyConfig { enabled: boolean; - modes: string[]; + /** Modes that must stay quiet; all others push. Empty array = push everywhere. */ + excludedModes: string[]; timeoutMs: number; shutdownTimeoutMs: number; dedupeMs: number; @@ -51,6 +52,14 @@ export interface NotifyConfig { exists: boolean; } +export const ALL_MODES = [ + "tui", + "json", + "rpc", + "print", +] as const; +export type KnownMode = (typeof ALL_MODES)[number]; + export interface NotifyPayload { event: NotifyEvent; status: string;