@xgjktech/xg-openclaw-harness-tools 0.2.0 → 0.3.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 CHANGED
@@ -3,26 +3,22 @@
3
3
  OpenClaw 第三方工具插件:企业计划工具 `xg_plan_write` / `xg_plan_execute`。
4
4
 
5
5
  用一份持久化的计划(SQLite 落盘)取代内置 `update_plan`(无状态、只回显、不落库),
6
- 并把推进进度尽力而为地通知到相关会话/群。
6
+ `xg_plan_write` 整张维护草稿计划,`xg_plan_execute` 只读查看计划或最近摘要。
7
7
 
8
8
  ## 架构裁决:实时事件当前处于事实性休眠(务必先读)
9
9
 
10
- **当前 OpenClaw 2026.6.10 版本下,缝隙B(实时事件推送)事实性休眠**:`xg_plan_execute`
11
- `execute`/`factory` 期都没有正当途径获取当次 agent run 的真实 `runId`(详见
12
- `docs/notes-plugin-entry.md` N2 的逐路径排查记录)。工具因此把 `routing.runId` 固定传空串,
13
- `notify.emitPlanEvent` 据此恒定返回 `{emitted:false,reason:"no runId"}`,**不会真的发出事件**
14
- (不伪造 runId 是硬性红线,见 `docs/04-互通设计.md` §2.4)。
10
+ **当前 OpenClaw 2026.6.10 版本下,实时事件推送链路休眠**:工具 execute/factory 期没有
11
+ 正当途径获取真实 runId(详见 `docs/notes-plugin-entry.md` N2),且 0.3.0 已移除工具侧
12
+ `notify.ts`。`xg_plan_write` 落库后不发事件,`xg_plan_execute` 全程只读。
15
13
 
16
14
  **因此 IM 侧(或任何下游消费方)不得依赖实时事件感知计划进度。** 当前唯一可靠的主感知路径是:
17
15
 
18
- 1. **SQLite 读取**——用 `openPlanStoreReadonly(dbPath)` 只读反查 `<stateDir>/xgjktech/plans.sqlite`,
19
- 这是权威、始终正确的状态源(工具每次推进都先落盘、落盘成功才尝试发事件,见 `03-工具契约.md` §2.5)。
16
+ 1. **SQLite 读取**——IM `openPlanStoreReadonly(dbPath)` 按 origin 只读反查
17
+ `<stateDir>/xgjktech/plans.sqlite` 并推送全量快照,这是权威状态源。
20
18
  2. **agent 最终回复**——agent 完成任务后的文本回复本身会体现进度。
21
19
 
22
- 事件契约(`PlanEventPayload` / `isPlanEventPayload` / `planEventStream`)已按正式设计原样实现并保留,
23
- **不是搁置未完成**,而是"契约就绪、通路等待宿主开放":一旦未来某个 OpenClaw 版本给插件暴露了
24
- 获取当次 `runId` 的 API,事件推送即可在**不改本插件代码**的前提下自动激活(`notify.ts` 的判断只看
25
- `payload.runId` 是否非空)。
20
+ 事件契约(`PlanEventPayload` / `isPlanEventPayload` / `planEventStream`)在 shared 中原样保留,
21
+ 仅用于下游编译兼容。未来恢复实时事件需要重新实现工具侧生产与 IM 侧消费,并单独发布。
26
22
 
27
23
  ## 环境要求
28
24
 
@@ -94,15 +90,13 @@ P1 阶段不锁死具体安装命令,避免与实际安装流程漂移(详
94
90
 
95
91
  ### `xg_plan_write`
96
92
 
97
- 编写/覆盖一份完整计划(goal + tasks[],task 可含 steps[]),写入 SQLite。用于任务开始时
98
- 建立计划,或对既有计划做整体重写。
93
+ 整张创建或覆盖一份草稿计划(goal + tasks[])并写入 SQLite。更新时必须重交所有要保留的
94
+ task;写入 schema 不接受 steps,但读取旧计划仍兼容旧 steps 数据。
99
95
 
100
96
  ### `xg_plan_execute`
101
97
 
102
- 推进一份已保存计划的单个状态(`start_task` / `complete_task` / `block_task` /
103
- `start_step` / `complete_step`),每次一步。落盘顺序:**先读最新状态 → 应用动作 →
104
- 不变量校验 → 落盘 → 尽力而为发事件**(先落盘、事件不影响主流程成败)。`block_task`
105
- 必须携带非空 `note` 说明阻塞原因。
98
+ 只读查看计划。带 planId 返回完整紧凑 view;不带 planId 返回最近 10 条计划摘要,用于跨会话
99
+ 找回 planId。更新进度统一使用 `xg_plan_write` 整张重交。
106
100
 
107
101
  两个工具的完整参数 schema 见 `src/plan-write-tool.ts` / `src/plan-execute-tool.ts`
108
102
  中的 `PLAN_WRITE_DESCRIPTION` / `PLAN_EXECUTE_DESCRIPTION`。
@@ -128,25 +122,23 @@ P1 阶段不锁死具体安装命令,避免与实际安装流程漂移(详
128
122
  3. 拉起一个真实 agent 会话,触发一次会列出可用工具的动作(例如让 agent 描述自己有哪些工具,
129
123
  或查看 gateway/日志里该次 attempt 实际注册的工具名列表)。
130
124
  4. 核对该工具列表:应包含 `xg_plan_write` 与 `xg_plan_execute`,**不应包含** `update_plan`。
131
- 5. 调用一次 `xg_plan_write` 建一份最小计划,再调用 `xg_plan_execute` 推进一步,确认返回
132
- `ok:true` `event` 字段为 `{emitted:false,reason:"no runId"}`(与本文档"架构裁决"章节
133
- 一致,属预期行为非故障);用 `openPlanStoreReadonly` 反查该 planId,确认落盘内容与预期一致。
125
+ 5. 调用一次 `xg_plan_write` 建一份最小计划,确认返回 `planId + view`;再用该 planId 调
126
+ `xg_plan_execute`,确认只读 view 一致。用 `openPlanStoreReadonly` 反查并核对落盘内容。
134
127
 
135
128
  **本项在本次交付中未在真实 OpenClaw gateway 环境实机执行**——本仓库当前开发环境没有可用的
136
129
  真实 OpenClaw gateway/agent 运行环境,因此无法诚实地报告"已实拉 agent 验证通过"。以下是
137
130
  已经做过、可以作为间接佐证的验证,但**明确说明这不等于实机验证**:
138
131
 
139
- - T2 阶段(`docs/notes-plugin-entry.md` N3.3)用**手工构造的 mock `api`**(捕获
140
- `registerTool` 调用、手工构造 `toolContext.sessionKey` 等字段)驱动编译后的 `dist/index.js`
141
- 跑通过一次注册链路:`register(api)` → 捕获到 2 个工具的 `factory` 调用 `factory` 拿到
142
- `toolContext.sessionKey` 调用 `execute()` 返回结果里的 `sessionKey` 与传入值一致、
143
- `planCount` 来自真实 `SqlitePlanStore.list()` 查询。**这验证的是插件代码本身可以被正确加载
144
- 并注册出预期的 2 个工具**,但 mock `api` 不是真实 OpenClaw gateway 的 `tools.allow`/`deny`
145
- 策略引擎,**不能**佐证"生产 deny/allow 配置生效后工具清单精确符合预期"这一条——那一条
146
- 必须在真实 gateway 环境按上面 5 步实测。
132
+ - 当前 factory 集成测试用**手工构造的 mock `api`**捕获 2 个工具的注册工厂,并提供带
133
+ `sessionKey`/`deliveryContext` `toolContext`:`xg_plan_write` 写入后通过只读存储反查
134
+ `origin`,`xg_plan_execute` 校验指定计划的 view 与最近计划列表。**这验证的是插件代码本身
135
+ 可以被正确加载、注册并贯通当前契约**,但 mock `api` 不是真实 OpenClaw gateway
136
+ `tools.allow`/`deny` 策略引擎,**不能**佐证"生产 deny/allow 配置生效后工具清单精确符合
137
+ 预期"这一条——那一条必须在真实 gateway 环境按上面 5 步实测。
147
138
  - T6 阶段(`packages/xg-harness-tools/src/integration.test.ts`)用真实的 `SqlitePlanStore`
148
139
  (临时目录)+ 真实工具函数(`buildPlanWriteTool`/`buildPlanExecuteTool`)跑通了完整的
149
- 写入→推进→落盘→只读反查链路,但同样不经过 OpenClaw 的工具 allowlist/deny 策略引擎。
140
+ 写入→整张重交→execute 只读查看→SQLite 只读反查链路,但同样不经过 OpenClaw 的工具
141
+ allowlist/deny 策略引擎。
150
142
 
151
143
  **结论:部署方在生产环境落实上述配置后,必须按"实机验证步骤"重新执行一次实拉 agent 验证,
152
144
  不能以本仓库现有的 mock/集成测试结果代替。**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAYA,wBAwDG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAYA,wBAmCG"}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ const PLUGIN_ID = "xg-harness-tools";
7
7
  export default defineToolPlugin({
8
8
  id: PLUGIN_ID,
9
9
  name: "XG Harness Tools",
10
- description: "企业工具插件:计划编写与推进(plan_write / plan_execute)",
10
+ description: "企业工具插件:计划编写与查看(plan_write / plan_execute)",
11
11
  tools: (tool) => {
12
12
  // store 单例:defineToolPlugin 的 tools(...) 只在插件加载时调用一次,
13
13
  // 闭包在此建一次即可;factory 本身会在每次 agent attempt 重建工具集时重新调用。
@@ -30,31 +30,11 @@ export default defineToolPlugin({
30
30
  }),
31
31
  tool({
32
32
  name: "xg_plan_execute",
33
- label: "【xg-harness】推进计划",
33
+ label: "【xg-harness】查看计划",
34
34
  description: PLAN_EXECUTE_DESCRIPTION,
35
35
  parameters: PlanExecuteParams,
36
36
  optional: true,
37
- // T3 已实证(docs/notes-plugin-entry.md N2):execute/factory 期均无正当 runId
38
- // 获取路径,routing.runId 只能留空——notify.emitPlanEvent 据此走
39
- // {emitted:false,reason:"no runId"} 降级分支,不伪造 runId(04-互通设计 §2.4 红线1)。
40
- factory: ({ api, toolContext }) => buildPlanExecuteTool({
41
- store: getStore(api),
42
- pluginId: PLUGIN_ID,
43
- emitAgentEvent: (params) => api.agent.events.emitAgentEvent(params),
44
- routing: {
45
- runId: "",
46
- ...(toolContext.sessionKey !== undefined ? { sessionKey: toolContext.sessionKey } : {}),
47
- ...(toolContext.deliveryContext !== undefined
48
- ? { deliveryContext: toolContext.deliveryContext }
49
- : {}),
50
- ...(toolContext.agentAccountId !== undefined
51
- ? { agentAccountId: toolContext.agentAccountId }
52
- : {}),
53
- ...(toolContext.messageChannel !== undefined
54
- ? { messageChannel: toolContext.messageChannel }
55
- : {}),
56
- },
57
- }),
37
+ factory: ({ api }) => buildPlanExecuteTool({ store: getStore(api) }),
58
38
  }),
59
39
  ];
60
40
  },
@@ -1,26 +1,12 @@
1
1
  import { Type, type Static } from "typebox";
2
2
  import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry";
3
- import { type PlanStore } from "@xgjktech/xg-openclaw-shared";
4
- import { type EmitAgentEventFn } from "./notify.js";
3
+ import type { PlanStore } from "@xgjktech/xg-openclaw-shared";
5
4
  export declare const PlanExecuteParams: Type.TObject<{
6
- planId: Type.TString;
7
- action: Type.TUnion<[Type.TLiteral<"start_task">, Type.TLiteral<"complete_task">, Type.TLiteral<"block_task">, Type.TLiteral<"start_step">, Type.TLiteral<"complete_step">]>;
8
- taskIndex: Type.TInteger;
9
- stepIndex: Type.TOptional<Type.TInteger>;
10
- note: Type.TOptional<Type.TString>;
5
+ planId: Type.TOptional<Type.TString>;
11
6
  }>;
12
7
  export type PlanExecuteParamsT = Static<typeof PlanExecuteParams>;
13
8
  export declare const PLAN_EXECUTE_DESCRIPTION: string;
14
9
  export declare function buildPlanExecuteTool(deps: {
15
10
  store: PlanStore;
16
- pluginId: string;
17
- emitAgentEvent: EmitAgentEventFn;
18
- routing: {
19
- runId: string;
20
- sessionKey?: string;
21
- deliveryContext?: unknown;
22
- agentAccountId?: string;
23
- messageChannel?: string;
24
- };
25
11
  }): AnyAgentTool;
26
12
  //# sourceMappingURL=plan-execute-tool.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plan-execute-tool.d.ts","sourceRoot":"","sources":["../src/plan-execute-tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAKL,KAAK,SAAS,EAEf,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAiB,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEnE,eAAO,MAAM,iBAAiB;;;;;;EAsB7B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,eAAO,MAAM,wBAAwB,QAazB,CAAC;AAiFb,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,gBAAgB,CAAC;IACjC,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,eAAe,CAAC,EAAE,OAAO,CAAC;QAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH,GAAG,YAAY,CA8Df"}
1
+ {"version":3,"file":"plan-execute-tool.d.ts","sourceRoot":"","sources":["../src/plan-execute-tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,8BAA8B,CAAC;AAI3E,eAAO,MAAM,iBAAiB;;EAK7B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,eAAO,MAAM,wBAAwB,QAOzB,CAAC;AAOb,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,GAAG,YAAY,CAsC7E"}
@@ -1,156 +1,47 @@
1
1
  import { Type } from "typebox";
2
- import { assertPlanInvariants, PlanInvariantError, } from "@xgjktech/xg-openclaw-shared";
2
+ import { renderPlanView } from "./plan-view.js";
3
3
  import { toolJsonResult } from "./tool-json-result.js";
4
- import { emitPlanEvent } from "./notify.js";
5
4
  export const PlanExecuteParams = Type.Object({
6
- planId: Type.String({ minLength: 1, description: "要推进的计划 id" }),
7
- action: Type.Union([
8
- Type.Literal("start_task"),
9
- Type.Literal("complete_task"),
10
- Type.Literal("block_task"),
11
- Type.Literal("start_step"),
12
- Type.Literal("complete_step"),
13
- ], { description: "推进动作" }),
14
- taskIndex: Type.Integer({ minimum: 0, description: "目标 task 在 tasks[] 的下标" }),
15
- stepIndex: Type.Optional(Type.Integer({ minimum: 0, description: "start_step/complete_step 时必填,目标 step 下标" })),
16
- note: Type.Optional(Type.String({ description: "block_task 时必填,说明阻塞原因;其他动作可选备注" })),
5
+ planId: Type.Optional(Type.String({ description: "要查看的计划 id;留空则列出最近计划" })),
17
6
  }, { additionalProperties: false });
18
7
  export const PLAN_EXECUTE_DESCRIPTION = [
19
- "用途:推进一份已保存的计划,一次推进一步,并把进度实时通知到相关会话/群。",
8
+ "用途:只读查看已保存的计划;不执行任务,也不修改计划。",
20
9
  "",
21
- "执行纪律(务必遵守):",
22
- "- 一次一步:同一时刻整份计划最多一个进行中的 task,task 内最多一个进行中的 step。",
23
- " 想开始新任务前,先把当前任务收尾(completed 或 blocked)。",
24
- "- 验证后才 completed:只有真正做完并验证通过,才把 task/step 标 completed;",
25
- " 没验证就别标完成。",
26
- "- 遇阻塞就停下并记录:卡住时用 action=block_task 并在 note 写清楚卡在哪、需要谁/什么才能继续,",
27
- " 不要绕过、不要假装完成。阻塞会被推送到群里等人回复——这正是本工具的价值。",
10
+ "- 带 planId:返回该计划当前的完整清单。",
11
+ "- 不带 planId:返回最近计划的摘要列表,用于忘记或跨会话找回 planId。",
28
12
  "",
29
- "参数:planId + action + taskIndex(+ 视情况 stepIndex / note)。",
30
- "每次成功推进都会落盘并发出进度事件;调用方无需手动通知。",
13
+ "需要更新进度时,用 xg_plan_write 重新提交整张计划。",
31
14
  ].join("\n");
32
- class PlanExecuteValidationError extends Error {
33
- code;
34
- constructor(code, message) {
35
- super(message ?? code);
36
- this.name = "PlanExecuteValidationError";
37
- this.code = code;
38
- }
39
- }
40
- function requireTask(plan, taskIndex) {
41
- const task = plan.tasks[taskIndex];
42
- if (task === undefined) {
43
- throw new PlanExecuteValidationError("INDEX_OUT_OF_RANGE", `taskIndex ${taskIndex} 越界`);
44
- }
45
- return task;
46
- }
47
- function requireStep(task, stepIndex) {
48
- const step = task.steps?.[stepIndex];
49
- if (step === undefined) {
50
- throw new PlanExecuteValidationError("INDEX_OUT_OF_RANGE", `stepIndex ${stepIndex} 越界`);
51
- }
52
- return step;
53
- }
54
- function applyAction(plan, params) {
55
- const task = requireTask(plan, params.taskIndex);
56
- switch (params.action) {
57
- case "start_task": {
58
- task.status = "in_progress";
59
- return;
60
- }
61
- case "complete_task": {
62
- task.status = "completed";
63
- return;
64
- }
65
- case "block_task": {
66
- if (params.note === undefined || params.note.trim() === "") {
67
- throw new PlanExecuteValidationError("NOTE_REQUIRED", "block_task 必须带非空 note");
68
- }
69
- task.status = "blocked";
70
- task.note = params.note;
71
- return;
72
- }
73
- case "start_step": {
74
- if (params.stepIndex === undefined) {
75
- throw new PlanExecuteValidationError("STEP_INDEX_REQUIRED", "start_step 必须带 stepIndex");
76
- }
77
- const step = requireStep(task, params.stepIndex);
78
- step.status = "in_progress";
79
- return;
80
- }
81
- case "complete_step": {
82
- if (params.stepIndex === undefined) {
83
- throw new PlanExecuteValidationError("STEP_INDEX_REQUIRED", "complete_step 必须带 stepIndex");
84
- }
85
- const step = requireStep(task, params.stepIndex);
86
- step.status = "completed";
87
- return;
88
- }
89
- }
90
- }
91
- function summarize(plan) {
92
- const summary = { total: 0, completed: 0, blocked: 0, inProgress: 0 };
93
- for (const task of plan.tasks) {
94
- summary.total += 1;
95
- if (task.status === "completed")
96
- summary.completed += 1;
97
- if (task.status === "blocked")
98
- summary.blocked += 1;
99
- if (task.status === "in_progress")
100
- summary.inProgress += 1;
101
- }
102
- return summary;
103
- }
104
15
  export function buildPlanExecuteTool(deps) {
105
- const { store, pluginId, emitAgentEvent, routing } = deps;
16
+ const { store } = deps;
106
17
  return {
107
18
  name: "xg_plan_execute",
108
- label: "【xg-harness】推进计划",
19
+ label: "【xg-harness】查看计划",
109
20
  description: PLAN_EXECUTE_DESCRIPTION,
110
21
  parameters: PlanExecuteParams,
111
22
  async execute(_toolCallId, rawParams) {
112
23
  const params = rawParams;
113
24
  try {
25
+ if (params.planId === undefined) {
26
+ const result = { ok: true, plans: store.list().slice(0, 10) };
27
+ return toolJsonResult(result);
28
+ }
114
29
  const plan = store.get(params.planId);
115
30
  if (plan === undefined) {
116
31
  const result = { ok: false, error: { code: "PLAN_NOT_FOUND" } };
117
32
  return toolJsonResult(result);
118
33
  }
119
- applyAction(plan, params);
120
- plan.updatedAt = new Date().toISOString();
121
- assertPlanInvariants(plan);
122
- store.set(plan);
123
- const task = plan.tasks[params.taskIndex];
124
- const payload = {
125
- kind: "plan_update",
34
+ const result = {
35
+ ok: true,
126
36
  planId: plan.planId,
127
- goal: plan.goal,
128
- action: params.action,
129
- taskIndex: params.taskIndex,
130
- ...(params.stepIndex !== undefined ? { stepIndex: params.stepIndex } : {}),
131
- taskTitle: task.title,
132
- taskStatus: task.status,
133
- ...(task.status === "blocked" && task.note !== undefined ? { blockedNote: task.note } : {}),
134
- summary: summarize(plan),
135
- runId: routing.runId,
136
- ...(routing.sessionKey !== undefined ? { sessionKey: routing.sessionKey } : {}),
137
- ...(routing.deliveryContext !== undefined ? { deliveryContext: routing.deliveryContext } : {}),
138
- ...(routing.agentAccountId !== undefined ? { agentAccountId: routing.agentAccountId } : {}),
139
- ...(routing.messageChannel !== undefined ? { messageChannel: routing.messageChannel } : {}),
37
+ view: renderPlanView(plan),
140
38
  };
141
- const event = emitPlanEvent({ pluginId, emitAgentEvent, payload });
142
- const result = { ok: true, planId: plan.planId, plan, event };
143
39
  return toolJsonResult(result);
144
40
  }
145
41
  catch (e) {
146
- const code = e instanceof PlanInvariantError
147
- ? "INVARIANT_VIOLATION"
148
- : e instanceof PlanExecuteValidationError
149
- ? e.code
150
- : "STORE_ERROR";
151
42
  const result = {
152
43
  ok: false,
153
- error: { code, detail: String(e.message) },
44
+ error: { code: "STORE_ERROR", detail: String(e.message) },
154
45
  };
155
46
  return toolJsonResult(result);
156
47
  }
@@ -0,0 +1,3 @@
1
+ import type { Plan } from "@xgjktech/xg-openclaw-shared";
2
+ export declare function renderPlanView(plan: Plan): string;
3
+ //# sourceMappingURL=plan-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-view.d.ts","sourceRoot":"","sources":["../src/plan-view.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAA0B,MAAM,8BAA8B,CAAC;AAoBjF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAejD"}
@@ -0,0 +1,26 @@
1
+ const TASK_STATUS_MARKERS = {
2
+ pending: "[ ]",
3
+ in_progress: "[>]",
4
+ completed: "[x]",
5
+ blocked: "[!]",
6
+ };
7
+ const STEP_STATUS_MARKERS = {
8
+ pending: "[ ]",
9
+ in_progress: "[>]",
10
+ completed: "[x]",
11
+ };
12
+ /** view 依赖"一行一项"的格式约定;字段内容里的换行统一压成空格,防止伪造出多余的行。 */
13
+ function singleLine(text) {
14
+ return text.replace(/\s*\r?\n\s*/g, " ");
15
+ }
16
+ export function renderPlanView(plan) {
17
+ const lines = [`goal: ${singleLine(plan.goal)}`];
18
+ plan.tasks.forEach((task, taskIndex) => {
19
+ const note = task.note !== undefined ? ` | note: ${singleLine(task.note)}` : "";
20
+ lines.push(`T${taskIndex} ${TASK_STATUS_MARKERS[task.status]} ${singleLine(task.title)}${note}`);
21
+ task.steps?.forEach((step, stepIndex) => {
22
+ lines.push(` T${taskIndex}.S${stepIndex} ${STEP_STATUS_MARKERS[step.status]} ${singleLine(step.text)}`);
23
+ });
24
+ });
25
+ return lines.join("\n");
26
+ }
@@ -8,10 +8,6 @@ export declare const PlanWriteParams: Type.TObject<{
8
8
  title: Type.TString;
9
9
  status: Type.TOptional<Type.TUnion<[Type.TLiteral<"pending">, Type.TLiteral<"in_progress">, Type.TLiteral<"completed">, Type.TLiteral<"blocked">]>>;
10
10
  note: Type.TOptional<Type.TString>;
11
- steps: Type.TOptional<Type.TArray<Type.TObject<{
12
- text: Type.TString;
13
- status: Type.TOptional<Type.TUnion<[Type.TLiteral<"pending">, Type.TLiteral<"in_progress">, Type.TLiteral<"completed">]>>;
14
- }>>>;
15
11
  }>>;
16
12
  }>;
17
13
  export type PlanWriteParamsT = Static<typeof PlanWriteParams>;
@@ -1 +1 @@
1
- {"version":3,"file":"plan-write-tool.d.ts","sourceRoot":"","sources":["../src/plan-write-tool.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,SAAS,EAKf,MAAM,8BAA8B,CAAC;AAoCtC,eAAO,MAAM,eAAe;;;;;;;;;;;;EAS3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,eAAO,MAAM,sBAAsB,QAcvB,CAAC;AAkDb,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,YAAY,CAwCtF"}
1
+ {"version":3,"file":"plan-write-tool.d.ts","sourceRoot":"","sources":["../src/plan-write-tool.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,SAAS,EAGf,MAAM,8BAA8B,CAAC;AA2BtC,eAAO,MAAM,eAAe;;;;;;;;EAiB3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,eAAO,MAAM,sBAAsB,QAkBvB,CAAC;AAwCb,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,YAAY,CA2CtF"}
@@ -1,48 +1,54 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { Type } from "typebox";
3
- import { assertPlanInvariants, PlanInvariantError, } from "@xgjktech/xg-openclaw-shared";
3
+ import { assertPlanInvariants, } from "@xgjktech/xg-openclaw-shared";
4
4
  import { toolJsonResult } from "./tool-json-result.js";
5
- const StepInput = Type.Object({
6
- text: Type.String({ minLength: 1, description: "单条可执行步骤,动词开头,无占位符" }),
7
- status: Type.Optional(Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")], { default: "pending" })),
8
- }, { additionalProperties: false });
5
+ import { renderPlanView } from "./plan-view.js";
9
6
  const TaskInput = Type.Object({
10
- title: Type.String({ minLength: 1, description: "任务标题,bite-size,单次可完成并验证" }),
7
+ title: Type.String({
8
+ minLength: 1,
9
+ maxLength: 200,
10
+ description: "能判断是否完成的具体任务",
11
+ }),
11
12
  status: Type.Optional(Type.Union([
12
13
  Type.Literal("pending"),
13
14
  Type.Literal("in_progress"),
14
15
  Type.Literal("completed"),
15
16
  Type.Literal("blocked"),
16
17
  ], { default: "pending" })),
17
- note: Type.Optional(Type.String({ description: "备注;status=blocked 时必填,说明阻塞原因" })),
18
- steps: Type.Optional(Type.Array(StepInput, { description: "可选子步骤" })),
18
+ note: Type.Optional(Type.String({ maxLength: 1000, description: "备注;blocked 时写明原因" })),
19
19
  }, { additionalProperties: false });
20
20
  export const PlanWriteParams = Type.Object({
21
- planId: Type.Optional(Type.String({ description: "留空=新建计划并返回新 planId;填写=整体改写该计划" })),
22
- goal: Type.String({ minLength: 1, description: "计划总目标,一句话说明完成态" }),
23
- tasks: Type.Array(TaskInput, { minItems: 1, description: "任务清单,至少一项" }),
21
+ planId: Type.Optional(Type.String({ maxLength: 128, description: "留空=新建;填写=整张覆盖该计划" })),
22
+ goal: Type.String({
23
+ minLength: 1,
24
+ maxLength: 500,
25
+ description: "计划总目标,一句话说明完成态",
26
+ }),
27
+ tasks: Type.Array(TaskInput, {
28
+ minItems: 1,
29
+ maxItems: 20,
30
+ description: "完整任务清单;更新时未提交的旧任务会被删除",
31
+ }),
24
32
  }, { additionalProperties: false });
25
33
  export const PLAN_WRITE_DESCRIPTION = [
26
- "用途:把一项工作拆成一份可执行计划并持久化保存(跨回合、跨天不丢失)。",
34
+ "用途:维护一份对用户可见、可跨回合保存的任务清单。每次调用都是整张替换,不是增量更新。",
27
35
  "",
28
- "何时用:接到多步骤任务、需要先规划再动手时,先用本工具写下计划。",
36
+ "何时不用(除非用户明确要求追踪):",
37
+ "- 纯问答、查询、解释或闲聊。",
38
+ "- 一两个动作即可完成,或预计当前回合可以直接做完。",
39
+ "- 不要把分析、思考和试错过程写成 task;只记录需要向用户展示的工作项。",
29
40
  "",
30
- "编写纪律(务必遵守):",
31
- "- Bite-size:每个 task 是一次可独立完成并验证的最小工作单元,别写成大而空的阶段。",
32
- "- 无占位符:task/step 文案必须是具体动作,禁止 \"TODO/待定/等等\" 这类占位。",
33
- "- 可验证:每个 task 应能明确判断\"做完没有\",把验证方式写进 title 或 note。",
34
- "- 顺序清晰:tasks 按依赖顺序排列,读者不需要猜先后。",
41
+ "何时用:",
42
+ "- 工作有 3 个以上可独立完成的任务,且预计需要多轮推进或中断后继续。",
43
+ "- 需要等待人的回复、审批或外部系统,即使任务较少。",
44
+ "- 用户明确要求任务清单或进度追踪。",
35
45
  "",
36
- "参数:goal=总目标;tasks=任务清单(含可选 steps 子步骤)。",
37
- "留空 planId=新建(返回新 planId 供后续 plan_execute 使用);填 planId=整体改写。",
38
- "不要在这里推进状态——推进用 xg_plan_execute",
46
+ "提交规则:",
47
+ "- 新建时不传 planId;更新时传入原 planId",
48
+ "- 更新时必须重新提交 goal 和所有要保留的 tasks,包括未变化任务及其当前 status/note。只提交变化项会删除其他任务;记不清时先用 xg_plan_execute 查看。",
49
+ "- goal 用一句话;tasks 通常 3-7 项,每项是能判断是否完成的具体动作,不写 TODO、待定或内部思考。",
50
+ "- status 为 pending、in_progress、completed 或 blocked。通常只有一项 in_progress,确实并行时可以有多项;blocked 时必须用 note 写明原因和继续所需条件。",
39
51
  ].join("\n");
40
- function toStep(input) {
41
- return {
42
- text: input.text,
43
- status: (input.status ?? "pending"),
44
- };
45
- }
46
52
  function toTask(input) {
47
53
  const task = {
48
54
  title: input.title,
@@ -51,9 +57,6 @@ function toTask(input) {
51
57
  if (input.note !== undefined) {
52
58
  task.note = input.note;
53
59
  }
54
- if (input.steps !== undefined) {
55
- task.steps = input.steps.map(toStep);
56
- }
57
60
  return task;
58
61
  }
59
62
  function buildPlan(params, existing, planId, origin) {
@@ -97,14 +100,17 @@ export function buildPlanWriteTool(store, origin) {
97
100
  const plan = buildPlan(params, existing, planId, origin);
98
101
  assertPlanInvariants(plan);
99
102
  store.set(plan);
100
- const result = { ok: true, planId: plan.planId, plan };
103
+ const result = {
104
+ ok: true,
105
+ planId: plan.planId,
106
+ view: renderPlanView(plan),
107
+ };
101
108
  return toolJsonResult(result);
102
109
  }
103
110
  catch (e) {
104
- const code = e instanceof PlanInvariantError ? "INVARIANT_VIOLATION" : "STORE_ERROR";
105
111
  const result = {
106
112
  ok: false,
107
- error: { code, detail: String(e.message) },
113
+ error: { code: "STORE_ERROR", detail: String(e.message) },
108
114
  };
109
115
  return toolJsonResult(result);
110
116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xgjktech/xg-openclaw-harness-tools",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "企业工具插件:plan_write / plan_execute 计划工具(OpenClaw 第三方工具插件)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "typebox": "1.1.39",
30
- "@xgjktech/xg-openclaw-shared": "^0.2.0"
30
+ "@xgjktech/xg-openclaw-shared": "^0.2.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.19.0",
package/dist/notify.d.ts DELETED
@@ -1,16 +0,0 @@
1
- import type { PluginAgentEventEmitParams, PluginAgentEventEmitResult } from "openclaw/plugin-sdk/plugin-entry";
2
- import { type PlanEventPayload } from "@xgjktech/xg-openclaw-shared";
3
- export type EmitAgentEventFn = (params: PluginAgentEventEmitParams) => PluginAgentEventEmitResult;
4
- /**
5
- * 缝隙B 事件发送:尽力而为,不因失败中断主流程(04-互通设计 §2.4 契约)。
6
- *
7
- * 红线:禁止伪造 runId(架构裁决 2026-07-24)。T3 已实证 factory ctx / execute ctx /
8
- * 插件可达的 runtime API 均无正当获取当次 run 真实 runId 的路径(见
9
- * docs/notes-plugin-entry.md N2)——payload.runId 缺失时直接跳过发送,不抛异常。
10
- */
11
- export declare function emitPlanEvent(params: {
12
- pluginId: string;
13
- emitAgentEvent: EmitAgentEventFn;
14
- payload: PlanEventPayload;
15
- }): PluginAgentEventEmitResult;
16
- //# sourceMappingURL=notify.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../src/notify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,0BAA0B,EAE3B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEtF,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,0BAA0B,KAAK,0BAA0B,CAAC;AAElG;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,gBAAgB,CAAC;IACjC,OAAO,EAAE,gBAAgB,CAAC;CAC3B,GAAG,0BAA0B,CAW7B"}
package/dist/notify.js DELETED
@@ -1,20 +0,0 @@
1
- import { planEventStream } from "@xgjktech/xg-openclaw-shared";
2
- /**
3
- * 缝隙B 事件发送:尽力而为,不因失败中断主流程(04-互通设计 §2.4 契约)。
4
- *
5
- * 红线:禁止伪造 runId(架构裁决 2026-07-24)。T3 已实证 factory ctx / execute ctx /
6
- * 插件可达的 runtime API 均无正当获取当次 run 真实 runId 的路径(见
7
- * docs/notes-plugin-entry.md N2)——payload.runId 缺失时直接跳过发送,不抛异常。
8
- */
9
- export function emitPlanEvent(params) {
10
- const { pluginId, emitAgentEvent, payload } = params;
11
- if (payload.runId.trim() === "") {
12
- return { emitted: false, reason: "no runId" };
13
- }
14
- return emitAgentEvent({
15
- runId: payload.runId,
16
- stream: planEventStream(pluginId),
17
- data: payload,
18
- ...(payload.sessionKey ? { sessionKey: payload.sessionKey } : {}),
19
- });
20
- }