ppxans-harness 2.4.0 → 2.6.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 +29 -32
- package/config/ppx.json +133 -151
- package/package.json +2 -5
- package/src/agent/index.js +3 -3
- package/src/agent/prompts.js +1 -1
- package/src/channels/http.js +76 -17
- package/src/config/providers.js +5 -11
- package/src/llm/client.js +187 -446
- package/src/llm/fence.js +8 -61
- package/src/llm/index.js +1 -1
- package/src/llm/router.js +5 -13
- package/src/mcp/admin.js +292 -0
- package/src/mcp/client.js +107 -5
- package/src/mcp/http.js +217 -0
- package/src/mcp/server.js +387 -0
- package/src/mcp/tasks.js +138 -0
- package/src/plugin/builtin.js +4 -1
package/src/mcp/tasks.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// src/mcp/tasks.js - 任务面板存储 (零依赖)
|
|
2
|
+
// 轻量任务工作区: 任务 = { id, title, description, status, steps[], result, createdAt, updatedAt }
|
|
3
|
+
// 持久化到 <root>/data/tasks.json (原子写), 支持 create/list/get/update/step/delete/complete。
|
|
4
|
+
// 供 MCP 工具 ppx.task.* 与 web 前端任务面板使用。
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { ensureDir, atomicWrite } from "../utils/store.js";
|
|
8
|
+
|
|
9
|
+
const VALID_TASK_STATUS = new Set(["todo", "running", "done", "failed"]);
|
|
10
|
+
const VALID_STEP_STATUS = new Set(["pending", "running", "done", "failed"]);
|
|
11
|
+
|
|
12
|
+
export class TaskBoard {
|
|
13
|
+
constructor(root) {
|
|
14
|
+
this.root = root;
|
|
15
|
+
this.file = path.join(root, "data", "tasks.json");
|
|
16
|
+
this.tasks = new Map();
|
|
17
|
+
this._load();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_load() {
|
|
21
|
+
try {
|
|
22
|
+
if (fs.existsSync(this.file)) {
|
|
23
|
+
const raw = JSON.parse(fs.readFileSync(this.file, "utf8"));
|
|
24
|
+
if (Array.isArray(raw.tasks)) {
|
|
25
|
+
for (const t of raw.tasks) {
|
|
26
|
+
if (t && t.id) this.tasks.set(t.id, this._normalize(t));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch (e) { /* 损坏则从空开始, 下次写入覆盖 */ }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_save() {
|
|
34
|
+
try {
|
|
35
|
+
ensureDir(path.dirname(this.file));
|
|
36
|
+
atomicWrite(this.file, JSON.stringify({ tasks: [...this.tasks.values()] }, null, 2));
|
|
37
|
+
} catch (e) { /* 持久化失败不阻断内存操作 */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_normalize(t) {
|
|
41
|
+
return {
|
|
42
|
+
id: String(t.id),
|
|
43
|
+
title: String(t.title || "未命名任务"),
|
|
44
|
+
description: String(t.description || ""),
|
|
45
|
+
status: VALID_TASK_STATUS.has(t.status) ? t.status : "todo",
|
|
46
|
+
steps: Array.isArray(t.steps) ? t.steps.map((s, i) => ({
|
|
47
|
+
title: String((s && s.title) || (typeof s === "string" ? s : `步骤 ${i + 1}`)),
|
|
48
|
+
status: VALID_STEP_STATUS.has(s && s.status) ? s.status : "pending",
|
|
49
|
+
detail: String((s && s.detail) || ""),
|
|
50
|
+
})) : [],
|
|
51
|
+
result: String(t.result || ""),
|
|
52
|
+
createdAt: t.createdAt || Date.now(),
|
|
53
|
+
updatedAt: t.updatedAt || Date.now(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_touch(t) { t.updatedAt = Date.now(); }
|
|
58
|
+
|
|
59
|
+
create({ title, description = "", steps = [] } = {}) {
|
|
60
|
+
if (!title) throw new Error("任务需 title");
|
|
61
|
+
const id = "t_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
62
|
+
const t = this._normalize({
|
|
63
|
+
id, title, description,
|
|
64
|
+
steps: steps.map((s) => ({ title: String(s) })).filter((s) => s.title),
|
|
65
|
+
});
|
|
66
|
+
this.tasks.set(id, t);
|
|
67
|
+
this._save();
|
|
68
|
+
return t;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get(id) { return this.tasks.get(String(id)) || null; }
|
|
72
|
+
|
|
73
|
+
list() {
|
|
74
|
+
const arr = [...this.tasks.values()].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
75
|
+
return {
|
|
76
|
+
tasks: arr,
|
|
77
|
+
counts: {
|
|
78
|
+
todo: arr.filter((t) => t.status === "todo").length,
|
|
79
|
+
running: arr.filter((t) => t.status === "running").length,
|
|
80
|
+
done: arr.filter((t) => t.status === "done").length,
|
|
81
|
+
failed: arr.filter((t) => t.status === "failed").length,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
update({ id, status, title, description } = {}) {
|
|
87
|
+
const t = this.get(id);
|
|
88
|
+
if (!t) throw new Error("任务不存在");
|
|
89
|
+
if (status != null) {
|
|
90
|
+
if (!VALID_TASK_STATUS.has(status)) throw new Error(`非法任务状态: ${status}`);
|
|
91
|
+
t.status = status;
|
|
92
|
+
}
|
|
93
|
+
if (title != null) t.title = String(title);
|
|
94
|
+
if (description != null) t.description = String(description);
|
|
95
|
+
this._touch(t);
|
|
96
|
+
this._save();
|
|
97
|
+
return t;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
step({ id, index, status, detail } = {}) {
|
|
101
|
+
const t = this.get(id);
|
|
102
|
+
if (!t) throw new Error("任务不存在");
|
|
103
|
+
const i = Number(index);
|
|
104
|
+
if (!Number.isInteger(i) || i < 0 || i >= t.steps.length) throw new Error("步骤 index 越界");
|
|
105
|
+
if (status != null) {
|
|
106
|
+
if (!VALID_STEP_STATUS.has(status)) throw new Error(`非法步骤状态: ${status}`);
|
|
107
|
+
t.steps[i].status = status;
|
|
108
|
+
}
|
|
109
|
+
if (detail != null) t.steps[i].detail = String(detail);
|
|
110
|
+
// 任一步骤 running → 任务 running; 全部 done → 任务 done (自动派生)
|
|
111
|
+
if (t.steps.some((s) => s.status === "running")) t.status = "running";
|
|
112
|
+
else if (t.steps.length && t.steps.every((s) => s.status === "done")) t.status = "done";
|
|
113
|
+
this._touch(t);
|
|
114
|
+
this._save();
|
|
115
|
+
return t;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
complete(id, result = "") {
|
|
119
|
+
const t = this.get(id);
|
|
120
|
+
if (!t) throw new Error("任务不存在");
|
|
121
|
+
for (const s of t.steps) if (s.status !== "failed") s.status = "done";
|
|
122
|
+
t.status = "done";
|
|
123
|
+
if (result) t.result = String(result).slice(0, 20000);
|
|
124
|
+
this._touch(t);
|
|
125
|
+
this._save();
|
|
126
|
+
return t;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
delete({ id } = {}) {
|
|
130
|
+
const ok = this.tasks.delete(String(id || ""));
|
|
131
|
+
if (ok) this._save();
|
|
132
|
+
return { ok };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createTaskBoard(root) {
|
|
137
|
+
return new TaskBoard(root);
|
|
138
|
+
}
|
package/src/plugin/builtin.js
CHANGED
|
@@ -26,7 +26,10 @@ import { exportMemorySnapshot, mergeSnapshotBack, hasSnapshot } from "../memory/
|
|
|
26
26
|
|
|
27
27
|
// fork 工具 (供 ctx.consume("fork") 取用)
|
|
28
28
|
const forkTools = { exportMemorySnapshot, mergeSnapshotBack, hasSnapshot };
|
|
29
|
-
export
|
|
29
|
+
// LLM 路由: import 本地绑定 + re-export 向后兼容 (v2.5.0 修复: 仅 export-from 不产生本地绑定,
|
|
30
|
+
// 导致 llmPlugin 里 resolveLLM/resolveAllLLMs 未定义, 插件装配被隔离且 LLM 实际未注入)
|
|
31
|
+
import { isUsableProvider, resolveLLM, resolveAllLLMs } from "../llm/router.js";
|
|
32
|
+
export { isUsableProvider, resolveLLM, resolveAllLLMs };
|
|
30
33
|
import { ModeRegistry, registerDefaultModes } from "../mode/index.js";
|
|
31
34
|
import { planExecExecutor } from "../mode/plan-exec.js";
|
|
32
35
|
import { routerExecutor } from "../mode/router.js";
|