ppxans-harness 2.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/LICENSE +201 -0
- package/README.md +265 -0
- package/bin/ppx-channels.js +3 -0
- package/bin/ppx-serve.js +6 -0
- package/bin/ppx.js +3 -0
- package/config/identity.md +6 -0
- package/config/ishiki.md +16 -0
- package/config/ppx.json +151 -0
- package/package.json +69 -0
- package/src/agent/context.js +179 -0
- package/src/agent/index.js +717 -0
- package/src/agent/prompts.js +107 -0
- package/src/aml-server.js +151 -0
- package/src/ans/eviction.js +144 -0
- package/src/ans/guard.js +120 -0
- package/src/ans/lifecycle.js +93 -0
- package/src/ans/proactive.js +129 -0
- package/src/ans/reward.js +112 -0
- package/src/ans/values.js +15 -0
- package/src/audit/audit-chain.js +167 -0
- package/src/audit/verifier.js +120 -0
- package/src/bus/circuit-breaker.js +115 -0
- package/src/bus/runtime-bus.js +94 -0
- package/src/channels/base.js +35 -0
- package/src/channels/feishu.js +127 -0
- package/src/channels/http.js +592 -0
- package/src/channels/index.js +110 -0
- package/src/channels/log.js +29 -0
- package/src/channels/wechat-crypto.js +74 -0
- package/src/channels/wechat.js +197 -0
- package/src/channels-cli.js +124 -0
- package/src/cli.js +120 -0
- package/src/config/channels.js +170 -0
- package/src/config/index.js +224 -0
- package/src/config/providers.js +189 -0
- package/src/config/settings.js +182 -0
- package/src/core/policy.js +272 -0
- package/src/core/trace.js +89 -0
- package/src/evolve/playbook.js +194 -0
- package/src/llm/client.js +446 -0
- package/src/llm/dsml.js +74 -0
- package/src/llm/embedder.js +35 -0
- package/src/llm/fence.js +105 -0
- package/src/llm/index.js +4 -0
- package/src/llm/retry.js +73 -0
- package/src/llm/router.js +98 -0
- package/src/mcp/client.js +375 -0
- package/src/mcp/index.js +116 -0
- package/src/memory/asset-hub.js +131 -0
- package/src/memory/canvas.js +131 -0
- package/src/memory/compaction.js +28 -0
- package/src/memory/experience.js +122 -0
- package/src/memory/fact-store.js +699 -0
- package/src/memory/failure-episode.js +99 -0
- package/src/memory/fork.js +83 -0
- package/src/memory/index.js +7 -0
- package/src/memory/l0.js +52 -0
- package/src/memory/l2.js +131 -0
- package/src/memory/l3.js +112 -0
- package/src/memory/memory-ticker.js +240 -0
- package/src/memory/session.js +398 -0
- package/src/mode/blackboard.js +49 -0
- package/src/mode/graph.js +41 -0
- package/src/mode/index.js +64 -0
- package/src/mode/legion.js +51 -0
- package/src/mode/plan-exec.js +50 -0
- package/src/mode/router.js +40 -0
- package/src/orchestrator/agent-worker.js +70 -0
- package/src/orchestrator/dag.js +83 -0
- package/src/orchestrator/index.js +2 -0
- package/src/orchestrator/legion.js +188 -0
- package/src/orchestrator/supervisor.js +177 -0
- package/src/persona/index.js +29 -0
- package/src/plugin/builtin.js +212 -0
- package/src/plugin/context.js +79 -0
- package/src/plugin/index.js +62 -0
- package/src/seam/registry.js +98 -0
- package/src/seam/shell.js +55 -0
- package/src/selfheal/evolve.js +68 -0
- package/src/selfheal/healer.js +167 -0
- package/src/selfheal/run.js +9 -0
- package/src/server.js +60 -0
- package/src/services/learning-service.js +177 -0
- package/src/services/memory-health.js +99 -0
- package/src/services/memory-service.js +160 -0
- package/src/skills/loader.js +150 -0
- package/src/skills/verify.js +100 -0
- package/src/tools/advanced.js +353 -0
- package/src/tools/builtin.js +298 -0
- package/src/tools/catalog.js +159 -0
- package/src/tools/command-guard.js +112 -0
- package/src/tools/custom.js +47 -0
- package/src/tools/delegate.js +297 -0
- package/src/tools/document.js +253 -0
- package/src/tools/governance.js +260 -0
- package/src/tools/index.js +11 -0
- package/src/tools/methods.js +178 -0
- package/src/tools/ocr.js +59 -0
- package/src/tools/seam.js +125 -0
- package/src/tools/selfmod.js +176 -0
- package/src/utils/logger.js +17 -0
- package/src/utils/pii.js +42 -0
- package/src/utils/store.js +108 -0
- package/src/utils/text.js +16 -0
- package/src/utils/trace.js +153 -0
- package/src/utils/winutf8.js +15 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/bus/runtime-bus.js - ②循环系: 全局 Runtime 总线 (RC1 §2 动态行为层地基)
|
|
2
|
+
// 在 session.js 会话事件日志之上新增一层"全局总线", 打通模块间函数直连的接缝:
|
|
3
|
+
// - Event: 事实广播 (发生了什么), 只读, 谁都能订阅
|
|
4
|
+
// - Command: 意图 (要执行什么), 带 id + 处理器, 返回 Result
|
|
5
|
+
// - State: 全局运行时状态槽 (可读写, 供①/⑦读倾向, 供观测)
|
|
6
|
+
// 零依赖, 纯 Node 原生。设计对齐 RC1 §5.2 总线消息类型。
|
|
7
|
+
export class RuntimeBus {
|
|
8
|
+
constructor({ name = "runtime", historyLimit = 200 } = {}) {
|
|
9
|
+
this.name = name;
|
|
10
|
+
this._events = new Map(); // type -> Set<handler>
|
|
11
|
+
this._handlers = new Map(); // verb -> handler
|
|
12
|
+
this._state = new Map(); // 状态槽
|
|
13
|
+
this._history = []; // 最近事件 (可观测/审计)
|
|
14
|
+
this._historyLimit = historyLimit;
|
|
15
|
+
this._seq = 0; // 全局序列
|
|
16
|
+
this._interceptors = []; // 命令前置拦截器 (⑧免疫可挂闸门)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---------- Event (事实广播, 只读) ----------
|
|
20
|
+
emit(type, payload = {}, meta = {}) {
|
|
21
|
+
this._seq++;
|
|
22
|
+
const ev = { seq: this._seq, ts: Date.now(), type, payload, meta };
|
|
23
|
+
this._history.push(ev);
|
|
24
|
+
if (this._history.length > this._historyLimit) this._history.shift();
|
|
25
|
+
const set = this._events.get(type);
|
|
26
|
+
if (set) for (const fn of set) { try { fn(ev); } catch {} }
|
|
27
|
+
return ev;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
on(type, handler) {
|
|
31
|
+
if (!this._events.has(type)) this._events.set(type, new Set());
|
|
32
|
+
this._events.get(type).add(handler);
|
|
33
|
+
return () => this.off(type, handler);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
off(type, handler) {
|
|
37
|
+
const set = this._events.get(type);
|
|
38
|
+
if (set) set.delete(handler);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------- Command (意图 + 处理器, 返回 Result) ----------
|
|
42
|
+
command(verb, payload = {}, { timeoutMs = 0 } = {}) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const handler = this._handlers.get(verb);
|
|
45
|
+
if (!handler) return resolve({ ok: false, error: `no-command-handler:${verb}`, payload });
|
|
46
|
+
let done = false;
|
|
47
|
+
const finish = (r) => { if (done) return; done = true; resolve(r); };
|
|
48
|
+
const id = `${verb}:${this._seq++}`;
|
|
49
|
+
const cmd = { id, ts: Date.now(), verb, payload };
|
|
50
|
+
// 拦截器链 (⑧免疫: 可在执行前做权限/风险校验, 拒绝则短路)
|
|
51
|
+
const run = async (i) => {
|
|
52
|
+
if (i >= this._interceptors.length) {
|
|
53
|
+
try { finish(await handler(cmd)); }
|
|
54
|
+
catch (e) { finish({ ok: false, error: String(e?.message || e), cmdId: id }); }
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await this._interceptors[i](cmd, () => run(i + 1));
|
|
59
|
+
} catch (e) {
|
|
60
|
+
finish({ ok: false, error: String(e?.message || e), cmdId: id, blocked: true });
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
run(0);
|
|
64
|
+
if (timeoutMs > 0) setTimeout(() => finish({ ok: false, error: "command-timeout", cmdId: id }), timeoutMs);
|
|
65
|
+
this.emit("command", { id, verb, payload });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
register(verb, handler) { this._handlers.set(verb, handler); return this; }
|
|
70
|
+
|
|
71
|
+
// 命令前置拦截器 (⑧免疫闸门挂载点)
|
|
72
|
+
intercept(fn) { this._interceptors.push(fn); return () => { const i = this._interceptors.indexOf(fn); if (i >= 0) this._interceptors.splice(i, 1); }; }
|
|
73
|
+
|
|
74
|
+
// ---------- State (运行时状态槽) ----------
|
|
75
|
+
get(key, fallback) {
|
|
76
|
+
if (!this._state.has(key)) return fallback;
|
|
77
|
+
return this._state.get(key);
|
|
78
|
+
}
|
|
79
|
+
set(key, value) { this._state.set(key, value); this._state._rev = (this._state._rev || 0) + 1; this.emit("state", { key, value }); return value; }
|
|
80
|
+
has(key) { return this._state.has(key); }
|
|
81
|
+
|
|
82
|
+
// ---------- 观测/审计 ----------
|
|
83
|
+
// 最近事件 (含 command), 供 trace/调试/审计拉取
|
|
84
|
+
recent(n = 50) { return this._history.slice(-n); }
|
|
85
|
+
// 挂总线之旅: 返回可断开回放的订阅器 (调试用)
|
|
86
|
+
trace() {
|
|
87
|
+
const out = [];
|
|
88
|
+
const off = this.on("*", (ev) => out.push(ev));
|
|
89
|
+
return { events: out, stop: off };
|
|
90
|
+
}
|
|
91
|
+
clearHistory() { this._history = []; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default RuntimeBus;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/channels/base.js - 通道基类
|
|
2
|
+
// 统一通道接口: 每个通道实现 connect/send/disconnect
|
|
3
|
+
// 可选能力: test() 连通性探测 (配置是否可用), mount(server) 把 webhook 路由挂到 HTTP server
|
|
4
|
+
export class Channel {
|
|
5
|
+
constructor(name, agent) {
|
|
6
|
+
this.name = name;
|
|
7
|
+
this.agent = agent;
|
|
8
|
+
this.connected = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async connect() { throw new Error(`${this.name}: connect() 未实现`); }
|
|
12
|
+
async send(to, text) { throw new Error(`${this.name}: send() 未实现`); }
|
|
13
|
+
async disconnect() { this.connected = false; }
|
|
14
|
+
|
|
15
|
+
// 连通性测试: 默认尝试 connect (验证配置能建立连接), 返回 { ok, detail }
|
|
16
|
+
// 需要网络探测的通道可覆盖为更细的校验 (如验证 token)
|
|
17
|
+
async test() {
|
|
18
|
+
try {
|
|
19
|
+
await this.connect();
|
|
20
|
+
return { ok: true, detail: `${this.name} 配置有效` };
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return { ok: false, detail: e.message };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 把 webhook 路由挂到 HTTP server (若通道是 webhook 型); 默认不挂载
|
|
27
|
+
mount(/* httpServer */) { return null; }
|
|
28
|
+
|
|
29
|
+
// 收到消息 → 调 agent 处理 → 回发
|
|
30
|
+
async handleMessage(from, text) {
|
|
31
|
+
const reply = await this.agent.chat(text);
|
|
32
|
+
await this.send(from, reply);
|
|
33
|
+
return reply;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/channels/feishu.js - 飞书通道适配器 (零依赖, 原生 fetch)
|
|
2
|
+
// 需要配置: app_id, app_secret, verify_token (config/channels.yaml)
|
|
3
|
+
// 用法: 飞书开放平台配置事件订阅 URL 指向本服务的 /feishu/webhook
|
|
4
|
+
import { Channel } from "./base.js";
|
|
5
|
+
|
|
6
|
+
export class FeishuChannel extends Channel {
|
|
7
|
+
constructor(agent, { appId, appSecret, verifyToken, webhookPath = "/feishu/webhook" } = {}) {
|
|
8
|
+
super("feishu", agent);
|
|
9
|
+
this.appId = appId || process.env.FEISHU_APP_ID || "";
|
|
10
|
+
this.appSecret = appSecret || process.env.FEISHU_APP_SECRET || "";
|
|
11
|
+
this.verifyToken = verifyToken || process.env.FEISHU_VERIFY_TOKEN || "";
|
|
12
|
+
this.webhookPath = webhookPath;
|
|
13
|
+
this.tenantToken = null;
|
|
14
|
+
this.tokenExpire = 0;
|
|
15
|
+
this.lastUser = null; // 最近收到消息的 open_id, 供主动提醒(广播)回发
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 主动提醒广播 (to="*") 时回最近联系人; 无记录则报缺接收人
|
|
19
|
+
_resolveTarget(to) {
|
|
20
|
+
const target = !to || to === "*" ? this.lastUser : to;
|
|
21
|
+
return target || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async _getTenantToken() {
|
|
25
|
+
if (this.tenantToken && Date.now() < this.tokenExpire) return this.tenantToken;
|
|
26
|
+
const r = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify({ app_id: this.appId, app_secret: this.appSecret }),
|
|
30
|
+
signal: AbortSignal.timeout(15000), // v1.0.8: 网络挂起不永久阻塞
|
|
31
|
+
});
|
|
32
|
+
const data = await r.json();
|
|
33
|
+
if (data.code !== 0) throw new Error(`飞书 token 失败: ${data.msg}`);
|
|
34
|
+
this.tenantToken = data.tenant_access_token;
|
|
35
|
+
this.tokenExpire = Date.now() + (data.expire - 60) * 1000;
|
|
36
|
+
return this.tenantToken;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async connect() {
|
|
40
|
+
if (!this.appId || !this.appSecret) {
|
|
41
|
+
throw new Error("飞书通道未配置: 需要 app_id + app_secret (config/channels.yaml 或环境变量)");
|
|
42
|
+
}
|
|
43
|
+
await this._getTenantToken(); // 验证凭证
|
|
44
|
+
this.connected = true;
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 连通性测试: 用已配置凭证换取 tenant token (真实网络验证)
|
|
49
|
+
async test() {
|
|
50
|
+
if (!this.appId || !this.appSecret) {
|
|
51
|
+
return { ok: false, detail: "未配置 app_id + app_secret" };
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const token = await this._getTenantToken();
|
|
55
|
+
return { ok: true, detail: `飞书凭证有效 (tenant_token 获取成功, ${String(token).slice(0, 6)}...)` };
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return { ok: false, detail: e.message };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 把 /feishu/webhook 挂到 HTTP server (webhook 型通道)
|
|
62
|
+
// v1.0.8: 注册到 HttpChannel 的 webhook 路由 (单一 request handler 分发, 无多 listener 竞态);
|
|
63
|
+
// 校验飞书事件订阅头 X-Lark-Request-Token
|
|
64
|
+
mount(server, httpChannel = null) {
|
|
65
|
+
const register = httpChannel && typeof httpChannel.registerWebhook === "function"
|
|
66
|
+
? (path, fn) => httpChannel.registerWebhook(path, fn)
|
|
67
|
+
: (path, fn) => { /* 无主通道时挂到 server (仅路径匹配, 调用方负责防双响应) */ server && server.on?.("request", fn); };
|
|
68
|
+
register(this.webhookPath, async (req, res) => {
|
|
69
|
+
let body = "";
|
|
70
|
+
for await (const c of req) body += c;
|
|
71
|
+
// 飞书事件订阅用请求头 X-Lark-Request-Token 携带 verify_token (body 内 token 极少存在, 仅作纵深)
|
|
72
|
+
if (this.verifyToken && req.headers["x-lark-request-token"] !== this.verifyToken) {
|
|
73
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
74
|
+
res.end(JSON.stringify({ error: "invalid token" }));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const out = await this.handleWebhook(body);
|
|
79
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
80
|
+
res.end(JSON.stringify(out));
|
|
81
|
+
} catch (e) {
|
|
82
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
83
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 处理飞书 webhook 事件 (事件订阅回调)
|
|
89
|
+
async handleWebhook(body) {
|
|
90
|
+
// 校验 verify_token (纵深防御; 主校验在 mount 的 X-Lark-Request-Token 头)
|
|
91
|
+
const data = typeof body === "string" ? JSON.parse(body) : body;
|
|
92
|
+
if (data.token && data.token !== this.verifyToken) {
|
|
93
|
+
return { code: 1, msg: "invalid token" };
|
|
94
|
+
}
|
|
95
|
+
// URL 验证 (飞书首次配置时)
|
|
96
|
+
if (data.type === "url_verification") {
|
|
97
|
+
return { challenge: data.challenge };
|
|
98
|
+
}
|
|
99
|
+
// 事件: 消息
|
|
100
|
+
const event = data.event;
|
|
101
|
+
if (event && event.type === "im.message.receive_v1" && event.message) {
|
|
102
|
+
const text = event.message.content ? JSON.parse(event.message.content).text || "" : "";
|
|
103
|
+
const openId = event.sender?.sender_id?.open_id || "";
|
|
104
|
+
if (openId) this.lastUser = openId; // 记录最近联系人, 供主动提醒回发
|
|
105
|
+
if (text) {
|
|
106
|
+
const reply = await this.agent.chat(text);
|
|
107
|
+
await this.send(openId || "*", reply);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { code: 0 };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async send(to, text) {
|
|
114
|
+
const target = this._resolveTarget(to);
|
|
115
|
+
if (!target) throw new Error("飞书主动推送缺接收人: 先收到过一条用户消息记录 open_id, 或用 send(openId, ...)");
|
|
116
|
+
const token = await this._getTenantToken();
|
|
117
|
+
const r = await fetch("https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
|
|
120
|
+
body: JSON.stringify({ receive_id: target, msg_type: "text", content: JSON.stringify({ text }) }),
|
|
121
|
+
signal: AbortSignal.timeout(15000), // v1.0.8: 网络挂起不永久阻塞
|
|
122
|
+
});
|
|
123
|
+
const data = await r.json();
|
|
124
|
+
if (data.code !== 0) throw new Error(`飞书发送失败: ${data.msg}`);
|
|
125
|
+
return data;
|
|
126
|
+
}
|
|
127
|
+
}
|