@xgjktech/xg-openclaw-shared 0.1.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.
@@ -0,0 +1,7 @@
1
+ export type { Plan, Task, Step, TaskStatus, StepStatus } from "./plan-types.js";
2
+ export { assertPlanInvariants, PlanInvariantError } from "./plan-types.js";
3
+ export type { PlanStore, PlanSummary } from "./plan-store.js";
4
+ export { SqlitePlanStore, openPlanStoreReadonly, resolvePlanDbPath, PlanStoreError } from "./plan-store.js";
5
+ export type { PlanEventPayload, PlanEventSummary } from "./plan-events.js";
6
+ export { planEventStream, isPlanEventPayload } from "./plan-events.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAChF,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE3E,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE5G,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // 桶文件:xg-shared 的唯一公开面。两插件(本插件 + IM 插件)均只从此入口 import。
2
+ export { assertPlanInvariants, PlanInvariantError } from "./plan-types.js";
3
+ export { SqlitePlanStore, openPlanStoreReadonly, resolvePlanDbPath, PlanStoreError } from "./plan-store.js";
4
+ export { planEventStream, isPlanEventPayload } from "./plan-events.js";
@@ -0,0 +1,40 @@
1
+ import type { TaskStatus } from "./plan-types.js";
2
+ /**
3
+ * stream 名 = `<pluginId>.plan`。第三方插件发的 stream 必须等于自己的 pluginId 或以
4
+ * `pluginId.` 前缀(OpenClaw 硬约束),禁止发裸 `plan`(host 保留流)。
5
+ */
6
+ export declare function planEventStream(pluginId: string): string;
7
+ export interface PlanEventSummary {
8
+ total: number;
9
+ completed: number;
10
+ blocked: number;
11
+ inProgress: number;
12
+ }
13
+ /**
14
+ * plan_execute 推进后发出的事件 payload(缝隙 A 定义,两插件共用)。
15
+ * 路由定位字段(deliveryContext/agentAccountId/messageChannel/sessionKey)均可选:
16
+ * 能从工具工厂 ctx 取到就带,取不到就省略——事件是尽力而为,不因缺字段而阻断。
17
+ */
18
+ export interface PlanEventPayload {
19
+ kind: "plan_update";
20
+ planId: string;
21
+ goal: string;
22
+ action: string;
23
+ taskIndex: number;
24
+ stepIndex?: number;
25
+ taskTitle: string;
26
+ taskStatus: TaskStatus;
27
+ blockedNote?: string;
28
+ summary: PlanEventSummary;
29
+ runId: string;
30
+ sessionKey?: string;
31
+ deliveryContext?: unknown;
32
+ agentAccountId?: string;
33
+ messageChannel?: string;
34
+ }
35
+ /**
36
+ * 运行时类型守卫:IM 侧订阅 handle 内用它校验 event.data 是否为合法 PlanEventPayload,
37
+ * 防止跨插件事件流误解析(订阅按 stream 过滤但 data 结构未经编译期保证)。
38
+ */
39
+ export declare function isPlanEventPayload(value: unknown): value is PlanEventPayload;
40
+ //# sourceMappingURL=plan-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-events.d.ts","sourceRoot":"","sources":["../src/plan-events.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKxD;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AA0BD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CA8B5E"}
@@ -0,0 +1,74 @@
1
+ // 缝隙B 事件契约:stream 名生成 + payload 类型 + 运行时类型守卫。
2
+ // 本文件不 import openclaw;emitAgentEvent 的实际调用在插件侧(xg-harness-tools/notify.ts)完成。
3
+ /**
4
+ * stream 名 = `<pluginId>.plan`。第三方插件发的 stream 必须等于自己的 pluginId 或以
5
+ * `pluginId.` 前缀(OpenClaw 硬约束),禁止发裸 `plan`(host 保留流)。
6
+ */
7
+ export function planEventStream(pluginId) {
8
+ if (pluginId.trim() === "") {
9
+ throw new Error("pluginId 不能为空");
10
+ }
11
+ return `${pluginId}.plan`;
12
+ }
13
+ function isPlanEventSummary(value) {
14
+ if (typeof value !== "object" || value === null) {
15
+ return false;
16
+ }
17
+ const summary = value;
18
+ return (typeof summary["total"] === "number" &&
19
+ typeof summary["completed"] === "number" &&
20
+ typeof summary["blocked"] === "number" &&
21
+ typeof summary["inProgress"] === "number");
22
+ }
23
+ const TASK_STATUSES = [
24
+ "pending",
25
+ "in_progress",
26
+ "completed",
27
+ "blocked",
28
+ ];
29
+ function isTaskStatusValue(value) {
30
+ return typeof value === "string" && TASK_STATUSES.includes(value);
31
+ }
32
+ /**
33
+ * 运行时类型守卫:IM 侧订阅 handle 内用它校验 event.data 是否为合法 PlanEventPayload,
34
+ * 防止跨插件事件流误解析(订阅按 stream 过滤但 data 结构未经编译期保证)。
35
+ */
36
+ export function isPlanEventPayload(value) {
37
+ if (typeof value !== "object" || value === null) {
38
+ return false;
39
+ }
40
+ const payload = value;
41
+ if (payload["kind"] !== "plan_update")
42
+ return false;
43
+ if (typeof payload["planId"] !== "string" || payload["planId"] === "")
44
+ return false;
45
+ if (typeof payload["goal"] !== "string")
46
+ return false;
47
+ if (typeof payload["action"] !== "string")
48
+ return false;
49
+ if (typeof payload["taskIndex"] !== "number")
50
+ return false;
51
+ if (payload["stepIndex"] !== undefined && typeof payload["stepIndex"] !== "number")
52
+ return false;
53
+ if (typeof payload["taskTitle"] !== "string")
54
+ return false;
55
+ if (!isTaskStatusValue(payload["taskStatus"]))
56
+ return false;
57
+ if (payload["blockedNote"] !== undefined && typeof payload["blockedNote"] !== "string") {
58
+ return false;
59
+ }
60
+ if (!isPlanEventSummary(payload["summary"]))
61
+ return false;
62
+ if (typeof payload["runId"] !== "string" || payload["runId"] === "")
63
+ return false;
64
+ if (payload["sessionKey"] !== undefined && typeof payload["sessionKey"] !== "string") {
65
+ return false;
66
+ }
67
+ if (payload["agentAccountId"] !== undefined && typeof payload["agentAccountId"] !== "string") {
68
+ return false;
69
+ }
70
+ if (payload["messageChannel"] !== undefined && typeof payload["messageChannel"] !== "string") {
71
+ return false;
72
+ }
73
+ return true;
74
+ }
@@ -0,0 +1,48 @@
1
+ import type { Plan } from "./plan-types.js";
2
+ export interface PlanSummary {
3
+ planId: string;
4
+ goal: string;
5
+ updatedAt: string;
6
+ taskCounts: {
7
+ total: number;
8
+ completed: number;
9
+ blocked: number;
10
+ inProgress: number;
11
+ };
12
+ }
13
+ export interface PlanStore {
14
+ get(planId: string): Plan | undefined;
15
+ set(plan: Plan): void;
16
+ delete(planId: string): void;
17
+ list(): PlanSummary[];
18
+ }
19
+ export declare class PlanStoreError extends Error {
20
+ constructor(message: string, options?: {
21
+ cause?: unknown;
22
+ });
23
+ }
24
+ /**
25
+ * SqlitePlanStore:本插件的唯一写者。读写走事务,异常一律转 PlanStoreError,
26
+ * 绝不让底层 SQLite 异常冒泡崩 gateway(企业工具与 gateway 同进程约束)。
27
+ */
28
+ export declare class SqlitePlanStore implements PlanStore {
29
+ private readonly db;
30
+ constructor(dbPath: string);
31
+ /** 关闭底层数据库连接,释放文件句柄。测试与短生命周期场景使用;插件运行时通常常驻不调用。 */
32
+ close(): void;
33
+ get(planId: string): Plan | undefined;
34
+ set(plan: Plan): void;
35
+ delete(planId: string): void;
36
+ list(): PlanSummary[];
37
+ }
38
+ /**
39
+ * IM 侧只读入口:以 readOnly 模式打开同一 DB,杜绝误写。
40
+ * 只暴露 get/list(缝隙 C 读路径),不暴露 set/delete。
41
+ */
42
+ export declare function openPlanStoreReadonly(dbPath: string): Pick<PlanStore, "get" | "list">;
43
+ /**
44
+ * 路径约定:<stateDir>/xgjktech/plans.sqlite(两插件共享 xgjktech 命名空间)。
45
+ * 负责 mkdir -p 该子目录,调用方无需自行建目录。
46
+ */
47
+ export declare function resolvePlanDbPath(stateDir: string): string;
48
+ //# sourceMappingURL=plan-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-store.d.ts","sourceRoot":"","sources":["../src/plan-store.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CACvF;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACtC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,IAAI,WAAW,EAAE,CAAC;CACvB;AAED,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI3D;AAyGD;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAC/C,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAe;gBAEtB,MAAM,EAAE,MAAM;IAI1B,kDAAkD;IAClD,KAAK,IAAI,IAAI;IAIb,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAIrC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IA8BrB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAQ5B,IAAI,IAAI,WAAW,EAAE;CAGtB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC,CAWrF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI1D"}
@@ -0,0 +1,185 @@
1
+ // PlanStore:Plan 的持久化访问器。node:sqlite 同步 API,单表 JSON blob + 冗余列。
2
+ // 本文件不 import openclaw;stateDir 字符串由调用方(插件)解析后传入。
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { mkdirSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ export class PlanStoreError extends Error {
7
+ constructor(message, options) {
8
+ super(message, options);
9
+ this.name = "PlanStoreError";
10
+ }
11
+ }
12
+ const SCHEMA_VERSION = 1;
13
+ const BUSY_TIMEOUT_MS = 5000;
14
+ const CREATE_TABLE_SQL = `
15
+ CREATE TABLE IF NOT EXISTS plans (
16
+ plan_id TEXT PRIMARY KEY,
17
+ goal TEXT NOT NULL,
18
+ created_at TEXT NOT NULL,
19
+ updated_at TEXT NOT NULL,
20
+ doc TEXT NOT NULL
21
+ ) STRICT;
22
+ `;
23
+ const CREATE_INDEX_SQL = `
24
+ CREATE INDEX IF NOT EXISTS idx_plans_updated_at ON plans(updated_at);
25
+ `;
26
+ function parsePlanDoc(row) {
27
+ try {
28
+ return JSON.parse(row.doc);
29
+ }
30
+ catch (cause) {
31
+ throw new PlanStoreError(`plan ${row.plan_id} 的 doc 列不是合法 JSON`, { cause });
32
+ }
33
+ }
34
+ function summarizeTaskCounts(plan) {
35
+ const counts = { total: 0, completed: 0, blocked: 0, inProgress: 0 };
36
+ for (const task of plan.tasks) {
37
+ counts.total += 1;
38
+ if (task.status === "completed")
39
+ counts.completed += 1;
40
+ if (task.status === "blocked")
41
+ counts.blocked += 1;
42
+ if (task.status === "in_progress")
43
+ counts.inProgress += 1;
44
+ }
45
+ return counts;
46
+ }
47
+ function ensureSchema(db) {
48
+ db.exec(CREATE_TABLE_SQL);
49
+ db.exec(CREATE_INDEX_SQL);
50
+ db.exec(`PRAGMA user_version = ${SCHEMA_VERSION};`);
51
+ }
52
+ function openDatabase(dbPath, options) {
53
+ try {
54
+ const db = new DatabaseSync(dbPath, { readOnly: options.readOnly });
55
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS};`);
56
+ if (!options.readOnly) {
57
+ db.exec("PRAGMA journal_mode = WAL;");
58
+ ensureSchema(db);
59
+ }
60
+ return db;
61
+ }
62
+ catch (cause) {
63
+ throw new PlanStoreError(`打开 PlanStore 数据库失败: ${dbPath}`, { cause });
64
+ }
65
+ }
66
+ function readPlanRow(db, planId) {
67
+ return db
68
+ .prepare("SELECT plan_id, goal, created_at, updated_at, doc FROM plans WHERE plan_id = ?")
69
+ .get(planId);
70
+ }
71
+ function readAllPlanRows(db) {
72
+ return db
73
+ .prepare("SELECT plan_id, goal, created_at, updated_at, doc FROM plans ORDER BY updated_at DESC")
74
+ .all();
75
+ }
76
+ function getPlan(db, planId, errorPrefix) {
77
+ try {
78
+ const row = readPlanRow(db, planId);
79
+ return row === undefined ? undefined : parsePlanDoc(row);
80
+ }
81
+ catch (cause) {
82
+ if (cause instanceof PlanStoreError)
83
+ throw cause;
84
+ throw new PlanStoreError(`${errorPrefix} plan ${planId} 失败`, { cause });
85
+ }
86
+ }
87
+ function listPlans(db, errorPrefix) {
88
+ try {
89
+ return readAllPlanRows(db).map((row) => {
90
+ const plan = parsePlanDoc(row);
91
+ return {
92
+ planId: row.plan_id,
93
+ goal: row.goal,
94
+ updatedAt: row.updated_at,
95
+ taskCounts: summarizeTaskCounts(plan),
96
+ };
97
+ });
98
+ }
99
+ catch (cause) {
100
+ if (cause instanceof PlanStoreError)
101
+ throw cause;
102
+ throw new PlanStoreError(`${errorPrefix} plan 列表失败`, { cause });
103
+ }
104
+ }
105
+ /**
106
+ * SqlitePlanStore:本插件的唯一写者。读写走事务,异常一律转 PlanStoreError,
107
+ * 绝不让底层 SQLite 异常冒泡崩 gateway(企业工具与 gateway 同进程约束)。
108
+ */
109
+ export class SqlitePlanStore {
110
+ db;
111
+ constructor(dbPath) {
112
+ this.db = openDatabase(dbPath, { readOnly: false });
113
+ }
114
+ /** 关闭底层数据库连接,释放文件句柄。测试与短生命周期场景使用;插件运行时通常常驻不调用。 */
115
+ close() {
116
+ this.db.close();
117
+ }
118
+ get(planId) {
119
+ return getPlan(this.db, planId, "读取");
120
+ }
121
+ set(plan) {
122
+ try {
123
+ this.db.exec("BEGIN IMMEDIATE");
124
+ try {
125
+ this.db
126
+ .prepare(`INSERT INTO plans (plan_id, goal, created_at, updated_at, doc)
127
+ VALUES (@planId, @goal, @createdAt, @updatedAt, @doc)
128
+ ON CONFLICT(plan_id) DO UPDATE SET
129
+ goal = excluded.goal,
130
+ updated_at = excluded.updated_at,
131
+ doc = excluded.doc`)
132
+ .run({
133
+ planId: plan.planId,
134
+ goal: plan.goal,
135
+ createdAt: plan.createdAt,
136
+ updatedAt: plan.updatedAt,
137
+ doc: JSON.stringify(plan),
138
+ });
139
+ this.db.exec("COMMIT");
140
+ }
141
+ catch (cause) {
142
+ this.db.exec("ROLLBACK");
143
+ throw cause;
144
+ }
145
+ }
146
+ catch (cause) {
147
+ throw new PlanStoreError(`写入 plan ${plan.planId} 失败`, { cause });
148
+ }
149
+ }
150
+ delete(planId) {
151
+ try {
152
+ this.db.prepare("DELETE FROM plans WHERE plan_id = ?").run(planId);
153
+ }
154
+ catch (cause) {
155
+ throw new PlanStoreError(`删除 plan ${planId} 失败`, { cause });
156
+ }
157
+ }
158
+ list() {
159
+ return listPlans(this.db, "列出");
160
+ }
161
+ }
162
+ /**
163
+ * IM 侧只读入口:以 readOnly 模式打开同一 DB,杜绝误写。
164
+ * 只暴露 get/list(缝隙 C 读路径),不暴露 set/delete。
165
+ */
166
+ export function openPlanStoreReadonly(dbPath) {
167
+ const db = openDatabase(dbPath, { readOnly: true });
168
+ return {
169
+ get(planId) {
170
+ return getPlan(db, planId, "只读读取");
171
+ },
172
+ list() {
173
+ return listPlans(db, "只读列出");
174
+ },
175
+ };
176
+ }
177
+ /**
178
+ * 路径约定:<stateDir>/xgjktech/plans.sqlite(两插件共享 xgjktech 命名空间)。
179
+ * 负责 mkdir -p 该子目录,调用方无需自行建目录。
180
+ */
181
+ export function resolvePlanDbPath(stateDir) {
182
+ const dir = join(stateDir, "xgjktech");
183
+ mkdirSync(dir, { recursive: true });
184
+ return join(dir, "plans.sqlite");
185
+ }
@@ -0,0 +1,35 @@
1
+ export type TaskStatus = "pending" | "in_progress" | "completed" | "blocked";
2
+ export type StepStatus = "pending" | "in_progress" | "completed";
3
+ export interface Step {
4
+ text: string;
5
+ status: StepStatus;
6
+ }
7
+ export interface Task {
8
+ title: string;
9
+ status: TaskStatus;
10
+ note?: string;
11
+ steps?: Step[];
12
+ }
13
+ export interface Plan {
14
+ planId: string;
15
+ goal: string;
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ tasks: Task[];
19
+ }
20
+ export declare class PlanInvariantError extends Error {
21
+ constructor(message: string);
22
+ }
23
+ /**
24
+ * 校验 Plan 是否满足全部领域不变量,违反则抛出 PlanInvariantError。
25
+ * plan_write / plan_execute 写库前必须调用;供两插件共享同一份判定,避免理解漂移。
26
+ *
27
+ * 覆盖的不变量(见 02-PlanStore设计 §2):
28
+ * 1. 全计划最多一个 in_progress task。
29
+ * 2. 每个 task 内最多一个 in_progress step。
30
+ * 3. status==="blocked" 的 task 必须有非空 note。
31
+ * 4. planId / goal 非空;tasks 至少一项。
32
+ * 5. task.status / step.status 取值必须在枚举内。
33
+ */
34
+ export declare function assertPlanInvariants(plan: Plan): void;
35
+ //# sourceMappingURL=plan-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-types.d.ts","sourceRoot":"","sources":["../src/plan-types.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,SAAS,CAAC;AAC7E,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,CAAC;AAUjE,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,IAAI;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAUD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAwDrD"}
@@ -0,0 +1,72 @@
1
+ // Plan/Task/Step 数据模型与不变量校验。纯类型 + 纯函数,无副作用、无 openclaw 依赖。
2
+ const TASK_STATUSES = [
3
+ "pending",
4
+ "in_progress",
5
+ "completed",
6
+ "blocked",
7
+ ];
8
+ const STEP_STATUSES = ["pending", "in_progress", "completed"];
9
+ export class PlanInvariantError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "PlanInvariantError";
13
+ }
14
+ }
15
+ function isTaskStatus(value) {
16
+ return TASK_STATUSES.includes(value);
17
+ }
18
+ function isStepStatus(value) {
19
+ return STEP_STATUSES.includes(value);
20
+ }
21
+ /**
22
+ * 校验 Plan 是否满足全部领域不变量,违反则抛出 PlanInvariantError。
23
+ * plan_write / plan_execute 写库前必须调用;供两插件共享同一份判定,避免理解漂移。
24
+ *
25
+ * 覆盖的不变量(见 02-PlanStore设计 §2):
26
+ * 1. 全计划最多一个 in_progress task。
27
+ * 2. 每个 task 内最多一个 in_progress step。
28
+ * 3. status==="blocked" 的 task 必须有非空 note。
29
+ * 4. planId / goal 非空;tasks 至少一项。
30
+ * 5. task.status / step.status 取值必须在枚举内。
31
+ */
32
+ export function assertPlanInvariants(plan) {
33
+ if (plan.planId.trim() === "") {
34
+ throw new PlanInvariantError("planId 不能为空");
35
+ }
36
+ if (plan.goal.trim() === "") {
37
+ throw new PlanInvariantError("goal 不能为空");
38
+ }
39
+ if (plan.tasks.length === 0) {
40
+ throw new PlanInvariantError("tasks 至少一项");
41
+ }
42
+ let inProgressTaskCount = 0;
43
+ plan.tasks.forEach((task, taskIndex) => {
44
+ if (!isTaskStatus(task.status)) {
45
+ throw new PlanInvariantError(`tasks[${taskIndex}].status 取值非法: ${task.status}`);
46
+ }
47
+ if (task.status === "in_progress") {
48
+ inProgressTaskCount += 1;
49
+ }
50
+ if (task.status === "blocked" && (task.note === undefined || task.note.trim() === "")) {
51
+ throw new PlanInvariantError(`tasks[${taskIndex}] 状态为 blocked 时必须带非空 note`);
52
+ }
53
+ if (task.steps === undefined) {
54
+ return;
55
+ }
56
+ let inProgressStepCount = 0;
57
+ task.steps.forEach((step, stepIndex) => {
58
+ if (!isStepStatus(step.status)) {
59
+ throw new PlanInvariantError(`tasks[${taskIndex}].steps[${stepIndex}].status 取值非法: ${step.status}`);
60
+ }
61
+ if (step.status === "in_progress") {
62
+ inProgressStepCount += 1;
63
+ }
64
+ });
65
+ if (inProgressStepCount > 1) {
66
+ throw new PlanInvariantError(`tasks[${taskIndex}] 内 in_progress 的 step 超过一个`);
67
+ }
68
+ });
69
+ if (inProgressTaskCount > 1) {
70
+ throw new PlanInvariantError("全计划 in_progress 的 task 超过一个");
71
+ }
72
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@xgjktech/xg-openclaw-shared",
3
+ "version": "0.1.0",
4
+ "description": "xgjktech OpenClaw 插件共享层:Plan 数据模型/不变量、SQLite PlanStore、计划事件契约",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=22.19.0"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "!dist/*.test.*"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "registry": "https://registry.npmjs.org/"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "typecheck": "tsc -p tsconfig.json --noEmit",
27
+ "pretest": "npm run build",
28
+ "test": "node --test dist/*.test.js",
29
+ "prepublishOnly": "npm run build && npm run test"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22.19.0",
33
+ "typescript": "^5.6.0"
34
+ }
35
+ }