ppxans-harness 2.4.0 → 3.2.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 +201 -201
- package/README.md +218 -265
- package/bin/ppx-channels.js +2 -2
- package/bin/ppx-serve.js +5 -5
- package/bin/ppx-setup.js +124 -0
- package/bin/ppx-web.js +140 -0
- package/bin/ppx.js +2 -2
- package/config/identity.md +6 -6
- package/config/ishiki.md +16 -16
- package/config/ppx.json +15 -151
- package/config/ppx.json.example +143 -0
- package/package.json +17 -10
- package/skills/.usage.json +6 -0
- package/skills/agent-professional-training/SKILL.md +94 -0
- package/skills/brainstorm/SKILL.md +24 -0
- package/skills/cupid-lover-comms/SKILL.md +37 -0
- package/skills/debug/SKILL.md +26 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/ponytail/SKILL.md +25 -0
- package/skills/ppx-memory/SKILL.md +91 -0
- package/skills/ppx-memory/scripts/cli.js +192 -0
- package/skills/ppx-memory/scripts/experience.js +133 -0
- package/skills/ppx-memory/scripts/fact-store.js +842 -0
- package/skills/ppx-memory/scripts/l0.js +52 -0
- package/skills/ppx-memory/scripts/l2.js +146 -0
- package/skills/ppx-memory/scripts/l3.js +112 -0
- package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
- package/skills/ppx-memory/scripts/pii.js +42 -0
- package/skills/ppx-memory/scripts/schema.js +80 -0
- package/skills/ppx-memory/scripts/session.js +398 -0
- package/skills/ppx-memory/scripts/similarity.js +43 -0
- package/skills/ppx-memory/scripts/store.js +116 -0
- package/skills/ppx-memory/scripts/wal.js +38 -0
- package/skills/ppx-selfheal/SKILL.md +24 -0
- package/skills/ppx-selfheal/scripts/cli.js +80 -0
- package/skills/ppx-selfheal/scripts/healer.js +184 -0
- package/skills/ppx-selfheal/scripts/logger.js +17 -0
- package/skills/ppx-selfheal/scripts/store.js +116 -0
- package/skills/prompt-depth-kit/SKILL.md +28 -0
- package/skills/session-naming/SKILL.md +36 -0
- package/skills/verify/SKILL.md +25 -0
- package/src/agent/index.js +1340 -717
- package/src/agent/prompts.js +47 -3
- package/src/aml-server.js +197 -151
- package/src/ans/eviction.js +123 -143
- package/src/ans/guard.js +159 -120
- package/src/ans/lifecycle.js +96 -93
- package/src/ans/proactive.js +112 -129
- package/src/ans/reward.js +95 -111
- package/src/ans/values.js +15 -15
- package/src/audit/audit-chain.js +43 -7
- package/src/audit/verifier.js +157 -120
- package/src/bus/circuit-breaker.js +9 -1
- package/src/bus/runtime-bus.js +107 -93
- package/src/channels/base.js +57 -34
- package/src/channels/feishu.js +118 -126
- package/src/channels/http.js +1090 -592
- package/src/channels/index.js +111 -110
- package/src/channels/log.js +29 -29
- package/src/channels/wechat-crypto.js +73 -74
- package/src/channels/wechat.js +191 -197
- package/src/channels/workspace.js +94 -0
- package/src/channels-cli.js +126 -124
- package/src/cli.js +129 -120
- package/src/commands/index.js +142 -0
- package/src/config/channels.js +137 -170
- package/src/config/index.js +277 -224
- package/src/config/placeholder.js +31 -0
- package/src/config/providers.js +148 -188
- package/src/config/settings.js +156 -182
- package/src/core/policy.js +69 -18
- package/src/core/trace.js +8 -6
- package/src/edit/editblock.js +266 -0
- package/src/edit/snapshot.js +67 -0
- package/src/evidence/index.js +153 -0
- package/src/evolve/playbook.js +9 -10
- package/src/hooks/index.js +113 -0
- package/src/llm/client.js +188 -446
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -105
- package/src/llm/index.js +4 -4
- package/src/llm/local-embedder.js +94 -0
- package/src/llm/presets.js +113 -0
- package/src/llm/pricing.js +93 -0
- package/src/llm/retry.js +73 -73
- package/src/llm/router.js +92 -97
- package/src/mcp/admin.js +326 -0
- package/src/mcp/client.js +487 -375
- package/src/mcp/http.js +203 -0
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +392 -0
- package/src/mcp/tasks.js +133 -0
- package/src/memory/asset-hub.js +6 -11
- package/src/memory/canvas.js +2 -5
- package/src/memory/compaction.js +28 -28
- package/src/memory/experience.js +133 -122
- package/src/memory/fact-store.js +914 -698
- package/src/memory/failure-episode.js +20 -11
- package/src/memory/fork.js +17 -8
- package/src/memory/index.js +8 -6
- package/src/memory/l0.js +53 -52
- package/src/memory/l2.js +145 -130
- package/src/memory/l3.js +111 -111
- package/src/memory/legion-board.js +71 -0
- package/src/memory/memory-ticker.js +239 -240
- package/src/memory/session.js +398 -397
- package/src/memory/sqlite-store.js +581 -0
- package/src/mode/blackboard.js +49 -49
- package/src/mode/graph.js +42 -41
- package/src/mode/index.js +64 -64
- package/src/mode/legion.js +54 -51
- package/src/mode/plan-exec.js +50 -50
- package/src/mode/router.js +28 -40
- package/src/orchestrator/agent-worker.js +69 -69
- package/src/orchestrator/dag.js +90 -83
- package/src/orchestrator/experts.js +76 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/legion.js +179 -187
- package/src/orchestrator/supervisor.js +6 -8
- package/src/permissions/index.js +378 -0
- package/src/persona/index.js +28 -29
- package/src/plugin/builtin.js +313 -212
- package/src/plugin/context.js +80 -79
- package/src/plugin/index.js +62 -62
- package/src/plugin/v3.js +73 -0
- package/src/protocol/index.js +148 -0
- package/src/repomap/index.js +309 -0
- package/src/review/index.js +393 -0
- package/src/seam/registry.js +3 -0
- package/src/seam/shell.js +55 -55
- package/src/security/injection.js +79 -0
- package/src/selfheal/evolve.js +67 -67
- package/src/selfheal/healer.js +184 -167
- package/src/selfheal/run.js +9 -9
- package/src/server.js +63 -60
- package/src/services/diagnose.js +180 -0
- package/src/services/learning-service.js +9 -0
- package/src/services/memory-health.js +34 -6
- package/src/services/memory-service.js +42 -9
- package/src/services/triage.js +138 -0
- package/src/session/parts.js +76 -0
- package/src/session/projection.js +73 -0
- package/src/session/rollout.js +54 -0
- package/src/session/turn.js +137 -0
- package/src/skills/lint.js +72 -0
- package/src/skills/loader.js +231 -150
- package/src/skills/search.js +58 -0
- package/src/skills/verify.js +95 -100
- package/src/tools/advanced.js +388 -352
- package/src/tools/builtin.js +384 -297
- package/src/tools/catalog.js +283 -159
- package/src/tools/command-guard.js +112 -112
- package/src/tools/custom.js +47 -47
- package/src/tools/delegate.js +383 -297
- package/src/tools/document.js +254 -253
- package/src/tools/git.js +151 -0
- package/src/tools/governance.js +47 -20
- package/src/tools/index.js +16 -11
- package/src/tools/methods.js +178 -178
- package/src/tools/ocr.js +59 -59
- package/src/tools/sandbox-worker.js +40 -0
- package/src/tools/sandbox.js +92 -0
- package/src/tools/seam.js +162 -125
- package/src/tools/selfmod.js +196 -176
- package/src/tools/v3.js +225 -0
- package/src/tools/vad.js +176 -0
- package/src/tools/voice.js +238 -0
- package/src/utils/async.js +14 -0
- package/src/utils/config-file.js +53 -0
- package/src/utils/crashguard.js +88 -0
- package/src/utils/http.js +53 -0
- package/src/utils/id.js +8 -0
- package/src/utils/json-state.js +33 -0
- package/src/utils/logger.js +17 -17
- package/src/utils/ndjson.js +25 -0
- package/src/utils/pii.js +42 -42
- package/src/utils/rate-limit.js +50 -0
- package/src/utils/schema.js +80 -0
- package/src/utils/similarity.js +43 -0
- package/src/utils/store.js +170 -108
- package/src/utils/text.js +15 -15
- package/src/utils/trace.js +153 -153
- package/src/utils/wal.js +39 -0
- package/src/utils/winutf8.js +16 -15
- package/src/wiki/index.js +170 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/commands/index.js - 斜杠命令统一模型 (吸收 claude-code types/command 思路, 纯 JS, 零依赖)
|
|
2
|
+
// Command = { name, description, argumentHint, isEnabled(ctx), run(ctx, args) -> 结构化意图 }
|
|
3
|
+
// run 只返回结构化意图对象 (如 {type:'intent', action:'new_session'}), 不直接操作 agent;
|
|
4
|
+
// 真正的落地由集成层(agent/通道)负责。保证内核与"命令做什么"解耦。
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
// ---- 命令注册表 ----
|
|
10
|
+
export function createCommandRegistry({ commands = [] } = {}) {
|
|
11
|
+
const map = new Map();
|
|
12
|
+
const register = (cmd) => {
|
|
13
|
+
if (!cmd || !cmd.name) throw new Error("命令缺少 name");
|
|
14
|
+
map.set(cmd.name, cmd);
|
|
15
|
+
return registry;
|
|
16
|
+
};
|
|
17
|
+
const get = (name) => map.get(name) || null;
|
|
18
|
+
const list = (enabledOnly = true) => {
|
|
19
|
+
const all = [...map.values()];
|
|
20
|
+
const filtered = enabledOnly
|
|
21
|
+
? all.filter((c) => (typeof c.isEnabled === "function" ? c.isEnabled(undefined) !== false : true))
|
|
22
|
+
: all;
|
|
23
|
+
return filtered.map((c) => ({ name: c.name, description: c.description || "", argumentHint: c.argumentHint || "" }));
|
|
24
|
+
};
|
|
25
|
+
// 解析 "/name args..." -> { cmd, args } 或 null
|
|
26
|
+
const parse = (input) => {
|
|
27
|
+
if (typeof input !== "string") return null;
|
|
28
|
+
const s = input.trim();
|
|
29
|
+
if (!s.startsWith("/")) return null;
|
|
30
|
+
const sp = s.indexOf(" ");
|
|
31
|
+
const name = (sp === -1 ? s.slice(1) : s.slice(1, sp)).trim();
|
|
32
|
+
const args = sp === -1 ? "" : s.slice(sp + 1).trim();
|
|
33
|
+
if (!name) return null;
|
|
34
|
+
return { cmd: name, args };
|
|
35
|
+
};
|
|
36
|
+
const execute = (input, ctx = {}) => {
|
|
37
|
+
const parsed = parse(input);
|
|
38
|
+
if (!parsed) return { type: "error", message: "无法解析命令, 应以 '/' 开头" };
|
|
39
|
+
const cmd = get(parsed.cmd);
|
|
40
|
+
if (!cmd) return { type: "error", message: `未知命令: /${parsed.cmd}` };
|
|
41
|
+
if (typeof cmd.isEnabled === "function" && cmd.isEnabled(ctx) === false) {
|
|
42
|
+
return { type: "message", content: `命令 /${cmd.name} 当前不可用` };
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return cmd.run(ctx, parsed.args, registry);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return { type: "error", message: `命令执行失败: ${e.message}` };
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const registry = { register, get, list, parse, execute, _map: map };
|
|
52
|
+
for (const c of commands) register(c);
|
|
53
|
+
return registry;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- 内置命令 ----
|
|
57
|
+
// 每个命令 run 返回结构化意图, 集成层据此行动
|
|
58
|
+
export const BUILTIN_COMMANDS = [
|
|
59
|
+
{ name: "new", description: "开启全新会话(清空当前上下文)", argumentHint: "", isEnabled: () => true,
|
|
60
|
+
run: () => ({ type: "intent", action: "new_session" }) },
|
|
61
|
+
{ name: "resume", description: "恢复最近的会话", argumentHint: "[sessionId]", isEnabled: () => true,
|
|
62
|
+
run: (_ctx, args) => ({ type: "intent", action: "resume_session", sessionId: args || null }) },
|
|
63
|
+
{ name: "compact", description: "压缩当前会话历史以节省上下文", argumentHint: "", isEnabled: () => true,
|
|
64
|
+
run: () => ({ type: "intent", action: "compact" }) },
|
|
65
|
+
{ name: "plan", description: "进入计划模式(先出计划再执行)", argumentHint: "", isEnabled: () => true,
|
|
66
|
+
run: () => ({ type: "intent", action: "enter_plan_mode" }) },
|
|
67
|
+
{ name: "review", description: "对当前变更执行分级代码审查", argumentHint: "[path|diff]", isEnabled: () => true,
|
|
68
|
+
run: (_ctx, args) => ({ type: "intent", action: "review", target: args || "." }) },
|
|
69
|
+
{ name: "init", description: "初始化项目上下文/记忆(PPX.md)", argumentHint: "", isEnabled: () => true,
|
|
70
|
+
run: () => ({ type: "intent", action: "init_project" }) },
|
|
71
|
+
{ name: "model", description: "查看或切换当前使用的模型", argumentHint: "[modelName]", isEnabled: () => true,
|
|
72
|
+
run: (_ctx, args) => ({ type: "intent", action: "set_model", model: args || null }) },
|
|
73
|
+
{ name: "status", description: "显示当前会话/agent 状态", argumentHint: "", isEnabled: () => true,
|
|
74
|
+
run: () => ({ type: "intent", action: "show_status" }) },
|
|
75
|
+
{ name: "memory", description: "查看/管理记忆层级", argumentHint: "[clear]", isEnabled: () => true,
|
|
76
|
+
run: (_ctx, args) => ({ type: "intent", action: "memory", sub: args || null }) },
|
|
77
|
+
{ name: "skills", description: "列出可用技能", argumentHint: "", isEnabled: () => true,
|
|
78
|
+
run: () => ({ type: "intent", action: "list_skills" }) },
|
|
79
|
+
{ name: "agents", description: "列出/派生子 agent(军团)", argumentHint: "[spawn|list]", isEnabled: () => true,
|
|
80
|
+
run: (_ctx, args) => ({ type: "intent", action: "agents", sub: args || "list" }) },
|
|
81
|
+
{ name: "goal", description: "打开/更新目标看板(Goal Board)", argumentHint: "[add|update|list]", isEnabled: () => true,
|
|
82
|
+
run: (_ctx, args) => ({ type: "intent", action: "goal_board", sub: args || "list" }) },
|
|
83
|
+
{ name: "help", description: "显示命令帮助", argumentHint: "", isEnabled: () => true,
|
|
84
|
+
run: (_ctx, _args, registry) => ({
|
|
85
|
+
type: "message",
|
|
86
|
+
content: "可用命令: " + (registry ? registry.list().map((c) => "/" + c.name).join(" ") : "/new /review /help ..."),
|
|
87
|
+
}) },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// 便捷: 创建带全部内置命令的注册表
|
|
91
|
+
export function createBuiltinRegistry() {
|
|
92
|
+
return createCommandRegistry({ commands: BUILTIN_COMMANDS });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 解析 .md 命令文件的 frontmatter (复用 skills 的轻量解析思路, 这里内联避免跨模块依赖)
|
|
96
|
+
function parseCommandMd(md) {
|
|
97
|
+
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
98
|
+
const meta = {};
|
|
99
|
+
if (m) {
|
|
100
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
101
|
+
const mm = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
102
|
+
if (mm) meta[mm[1].toLowerCase()] = mm[2].trim();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const body = m ? md.slice(m[0].length) : md;
|
|
106
|
+
return { meta, body: body.trim() };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 从 .ppx/commands/*.md 加载用户命令 (frontmatter: name/description, 正文作为 prompt 模板)
|
|
110
|
+
export function loadUserCommands(dir) {
|
|
111
|
+
const cmdsDir = path.join(dir, ".ppx", "commands");
|
|
112
|
+
const out = [];
|
|
113
|
+
if (!fs.existsSync(cmdsDir)) return out;
|
|
114
|
+
let entries;
|
|
115
|
+
try {
|
|
116
|
+
entries = fs.readdirSync(cmdsDir, { withFileTypes: true });
|
|
117
|
+
} catch {
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
for (const e of entries) {
|
|
121
|
+
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
122
|
+
const raw = fs.readFileSync(path.join(cmdsDir, e.name), "utf8");
|
|
123
|
+
const { meta, body } = parseCommandMd(raw);
|
|
124
|
+
const name = meta.name || e.name.replace(/\.md$/, "");
|
|
125
|
+
if (!name) continue;
|
|
126
|
+
out.push({
|
|
127
|
+
name,
|
|
128
|
+
description: meta.description || body.slice(0, 60),
|
|
129
|
+
argumentHint: meta.argumenthint || "",
|
|
130
|
+
isEnabled: () => true,
|
|
131
|
+
// 用户命令: 返回 prompt 模板意图, 由集成层注入 system/user
|
|
132
|
+
run: (_ctx, args) => ({ type: "prompt", command: name, template: body, args }),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 合并内置 + 用户命令, 返回完整注册表
|
|
139
|
+
export function createRegistryWithUserCommands(dir, { extra = [] } = {}) {
|
|
140
|
+
const user = loadUserCommands(dir);
|
|
141
|
+
return createCommandRegistry({ commands: [...BUILTIN_COMMANDS, ...user, ...extra] });
|
|
142
|
+
}
|
package/src/config/channels.js
CHANGED
|
@@ -1,170 +1,137 @@
|
|
|
1
|
-
// src/config/channels.js - 通道配置 CRUD (借鉴 providers.js 模式)
|
|
2
|
-
// 唯一事实源 = config/ppx.json 的 channels 对象; 原子写盘 + 备份
|
|
3
|
-
// 用途: `ppx channels` CLI / HTTP API 让用户自己连通道, 不改源码
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 更新通道配置 (patch 覆盖; 空字符串清空字段)
|
|
139
|
-
// v1.0.8: 写盘在文件锁内读-改-写 (防并发丢更新)
|
|
140
|
-
export function updateChannel(root, name, patch) {
|
|
141
|
-
return withFileLock(getConfigPath(root), () => {
|
|
142
|
-
const { channels, raw } = readChannels(root);
|
|
143
|
-
const { clean, errors } = validateChannel(name, patch);
|
|
144
|
-
if (errors.length) throw new Error(errors.join("; "));
|
|
145
|
-
const merged = { ...(channels[name] || {}), ...clean };
|
|
146
|
-
// 空字符串视为清空
|
|
147
|
-
for (const k of Object.keys(clean)) if (clean[k] === "") delete merged[k];
|
|
148
|
-
channels[name] = merged;
|
|
149
|
-
raw.channels = channels;
|
|
150
|
-
writeConfigAtomic(root, raw);
|
|
151
|
-
return sanitizeChannel(name, merged);
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 启用/禁用
|
|
156
|
-
export function setChannelEnabled(root, name, enabled) {
|
|
157
|
-
return updateChannel(root, name, { enabled: !!enabled });
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// 移除配置 (重置回默认, 即从 channels.<name> 删除)
|
|
161
|
-
export function removeChannel(root, name) {
|
|
162
|
-
return withFileLock(getConfigPath(root), () => {
|
|
163
|
-
const { channels, raw } = readChannels(root);
|
|
164
|
-
const existed = name in channels;
|
|
165
|
-
delete channels[name];
|
|
166
|
-
raw.channels = channels;
|
|
167
|
-
writeConfigAtomic(root, raw);
|
|
168
|
-
return existed;
|
|
169
|
-
});
|
|
170
|
-
}
|
|
1
|
+
// src/config/channels.js - 通道配置 CRUD (借鉴 providers.js 模式)
|
|
2
|
+
// 唯一事实源 = config/ppx.json 的 channels 对象; 原子写盘 + 备份
|
|
3
|
+
// 用途: `ppx channels` CLI / HTTP API 让用户自己连通道, 不改源码
|
|
4
|
+
import { withFileLock } from "../utils/store.js";
|
|
5
|
+
import { configFilePath, readPpxConfig, writeConfigAtomic } from "../utils/config-file.js";
|
|
6
|
+
|
|
7
|
+
// 每通道字段 schema: { type, label, def, secret }
|
|
8
|
+
// secret=true 的字段写盘存明文 (本地配置), 但 list/sanitize 时只暴露 "已设置" 标志
|
|
9
|
+
export const CHANNEL_SCHEMAS = {
|
|
10
|
+
http: {
|
|
11
|
+
enabled: { type: "boolean", label: "启用", def: true },
|
|
12
|
+
host: { type: "string", label: "监听地址", def: "127.0.0.1" },
|
|
13
|
+
port: { type: "number", label: "端口", def: 8899 },
|
|
14
|
+
auth_token: { type: "string", label: "鉴权 token (可选, 留空自动生成)", secret: true },
|
|
15
|
+
},
|
|
16
|
+
feishu: {
|
|
17
|
+
enabled: { type: "boolean", label: "启用", def: false },
|
|
18
|
+
appId: { type: "string", label: "App ID", prompt: "飞书开放平台应用的 App ID" },
|
|
19
|
+
appSecret: { type: "string", label: "App Secret", secret: true, prompt: "飞书开放平台应用的 App Secret" },
|
|
20
|
+
verifyToken: { type: "string", label: "Verification Token", secret: true, prompt: "事件订阅的 Verification Token" },
|
|
21
|
+
webhookPath: { type: "string", label: "Webhook 路径", def: "/feishu/webhook" },
|
|
22
|
+
},
|
|
23
|
+
wechat: {
|
|
24
|
+
enabled: { type: "boolean", label: "启用", def: false },
|
|
25
|
+
path: { type: "string", label: "Webhook 路径", def: "/wechat/webhook" },
|
|
26
|
+
token: { type: "string", label: "回调 Token", secret: true, prompt: "企业微信回调 Token" },
|
|
27
|
+
encodingAESKey: { type: "string", label: "EncodingAESKey", secret: true, prompt: "企业微信回调 EncodingAESKey" },
|
|
28
|
+
corpId: { type: "string", label: "企业 ID (corp_id)", prompt: "企业微信企业 ID, 用于主动推送" },
|
|
29
|
+
corpSecret: { type: "string", label: "企业密钥 (corp_secret)", secret: true, prompt: "企业微信应用 Secret, 用于主动推送" },
|
|
30
|
+
agentId: { type: "string", label: "应用 AgentID", prompt: "企业微信自建应用 AgentID, 用于主动推送" },
|
|
31
|
+
},
|
|
32
|
+
log: {
|
|
33
|
+
enabled: { type: "boolean", label: "启用", def: false },
|
|
34
|
+
target: { type: "string", label: "输出目标", def: "console" },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const CHANNEL_NAMES = Object.keys(CHANNEL_SCHEMAS);
|
|
39
|
+
|
|
40
|
+
const getConfigPath = configFilePath;
|
|
41
|
+
|
|
42
|
+
// 读取 config, 返回 { channels, raw }
|
|
43
|
+
export function readChannels(root) {
|
|
44
|
+
const raw = readPpxConfig(root, null);
|
|
45
|
+
if (raw === null) return { channels: {}, raw: { channels: {} } };
|
|
46
|
+
return { channels: raw.channels || {}, raw };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 校验 patch: 剔除未知字段 + 类型检查; 返回 { clean, errors[] }
|
|
50
|
+
export function validateChannel(name, patch) {
|
|
51
|
+
const schema = CHANNEL_SCHEMAS[name];
|
|
52
|
+
if (!schema) return { clean: {}, errors: [`未知通道类型: ${name}`] };
|
|
53
|
+
const clean = {};
|
|
54
|
+
const errors = [];
|
|
55
|
+
for (const [key, val] of Object.entries(patch || {})) {
|
|
56
|
+
if (!(key in schema)) { errors.push(`未知字段 ${name}.${key} (可用: ${Object.keys(schema).join(", ")})`); continue; }
|
|
57
|
+
const { type } = schema[key];
|
|
58
|
+
if (type === "number") {
|
|
59
|
+
if (val === "" || val == null) { clean[key] = val; continue; }
|
|
60
|
+
const n = Number(val);
|
|
61
|
+
if (!Number.isFinite(n)) { errors.push(`${name}.${key} 应为数字`); continue; }
|
|
62
|
+
clean[key] = n;
|
|
63
|
+
} else if (type === "boolean") {
|
|
64
|
+
// v1.0.8: 识别字符串 "false"/"0" (原 !!val 会把 "false" 当 true)
|
|
65
|
+
if (val === true || val === "true" || val === 1 || val === "1") clean[key] = true;
|
|
66
|
+
else if (val === false || val === "false" || val === 0 || val === "0") clean[key] = false;
|
|
67
|
+
else { errors.push(`${name}.${key} 应为布尔`); continue; }
|
|
68
|
+
} else {
|
|
69
|
+
clean[key] = String(val);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { clean, errors };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 脱敏视图: secret 字段只暴露 "已设置" 标志 (供 list 展示)
|
|
76
|
+
export function sanitizeChannel(name, cfg) {
|
|
77
|
+
const schema = CHANNEL_SCHEMAS[name] || {};
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const key of Object.keys(schema)) {
|
|
80
|
+
if (cfg[key] === undefined || cfg[key] === null || cfg[key] === "") continue;
|
|
81
|
+
if (schema[key].secret) out[`${key}_set`] = true;
|
|
82
|
+
else out[key] = cfg[key];
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 列出全部通道状态 (含默认值 + 脱敏)
|
|
88
|
+
export function listChannels(root) {
|
|
89
|
+
const { channels } = readChannels(root);
|
|
90
|
+
return CHANNEL_NAMES.map((name) => {
|
|
91
|
+
const schema = CHANNEL_SCHEMAS[name];
|
|
92
|
+
const cfg = { ...(channels[name] || {}) };
|
|
93
|
+
// 补默认值, 让 list 显示完整配置 (用户未写的字段显示默认)
|
|
94
|
+
for (const [key, f] of Object.entries(schema)) {
|
|
95
|
+
if (cfg[key] === undefined && f.def !== undefined) cfg[key] = f.def;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
name,
|
|
99
|
+
enabled: cfg.enabled ?? schema.enabled.def,
|
|
100
|
+
fields: sanitizeChannel(name, cfg),
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 更新通道配置 (patch 覆盖; 空字符串清空字段)
|
|
106
|
+
// v1.0.8: 写盘在文件锁内读-改-写 (防并发丢更新)
|
|
107
|
+
export function updateChannel(root, name, patch) {
|
|
108
|
+
return withFileLock(getConfigPath(root), () => {
|
|
109
|
+
const { channels, raw } = readChannels(root);
|
|
110
|
+
const { clean, errors } = validateChannel(name, patch);
|
|
111
|
+
if (errors.length) throw new Error(errors.join("; "));
|
|
112
|
+
const merged = { ...(channels[name] || {}), ...clean };
|
|
113
|
+
// 空字符串视为清空
|
|
114
|
+
for (const k of Object.keys(clean)) if (clean[k] === "") delete merged[k];
|
|
115
|
+
channels[name] = merged;
|
|
116
|
+
raw.channels = channels;
|
|
117
|
+
writeConfigAtomic(root, raw);
|
|
118
|
+
return sanitizeChannel(name, merged);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 启用/禁用
|
|
123
|
+
export function setChannelEnabled(root, name, enabled) {
|
|
124
|
+
return updateChannel(root, name, { enabled: !!enabled });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 移除配置 (重置回默认, 即从 channels.<name> 删除)
|
|
128
|
+
export function removeChannel(root, name) {
|
|
129
|
+
return withFileLock(getConfigPath(root), () => {
|
|
130
|
+
const { channels, raw } = readChannels(root);
|
|
131
|
+
const existed = name in channels;
|
|
132
|
+
delete channels[name];
|
|
133
|
+
raw.channels = channels;
|
|
134
|
+
writeConfigAtomic(root, raw);
|
|
135
|
+
return existed;
|
|
136
|
+
});
|
|
137
|
+
}
|