@mebular/fleet 0.0.0-stage → 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.
Files changed (178) hide show
  1. package/dist/agentdir.d.ts +43 -0
  2. package/dist/agentdir.d.ts.map +1 -0
  3. package/dist/agentdir.js +107 -0
  4. package/dist/agentdir.js.map +1 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +1135 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/collab/chatter.d.ts +54 -0
  10. package/dist/collab/chatter.d.ts.map +1 -0
  11. package/dist/collab/chatter.js +94 -0
  12. package/dist/collab/chatter.js.map +1 -0
  13. package/dist/collab/dag.d.ts +27 -0
  14. package/dist/collab/dag.d.ts.map +1 -0
  15. package/dist/collab/dag.js +107 -0
  16. package/dist/collab/dag.js.map +1 -0
  17. package/dist/collab/negotiation.d.ts +60 -0
  18. package/dist/collab/negotiation.d.ts.map +1 -0
  19. package/dist/collab/negotiation.js +90 -0
  20. package/dist/collab/negotiation.js.map +1 -0
  21. package/dist/collab/tree.d.ts +39 -0
  22. package/dist/collab/tree.d.ts.map +1 -0
  23. package/dist/collab/tree.js +177 -0
  24. package/dist/collab/tree.js.map +1 -0
  25. package/dist/config.d.ts +123 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +206 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/index.d.ts +33 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +40 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/join.d.ts +50 -0
  34. package/dist/join.d.ts.map +1 -0
  35. package/dist/join.js +167 -0
  36. package/dist/join.js.map +1 -0
  37. package/dist/jointoken.d.ts +144 -0
  38. package/dist/jointoken.d.ts.map +1 -0
  39. package/dist/jointoken.js +472 -0
  40. package/dist/jointoken.js.map +1 -0
  41. package/dist/model.d.ts +64 -0
  42. package/dist/model.d.ts.map +1 -0
  43. package/dist/model.js +158 -0
  44. package/dist/model.js.map +1 -0
  45. package/dist/onboard.d.ts +90 -0
  46. package/dist/onboard.d.ts.map +1 -0
  47. package/dist/onboard.js +510 -0
  48. package/dist/onboard.js.map +1 -0
  49. package/dist/protocol/envelope.d.ts +94 -0
  50. package/dist/protocol/envelope.d.ts.map +1 -0
  51. package/dist/protocol/envelope.js +169 -0
  52. package/dist/protocol/envelope.js.map +1 -0
  53. package/dist/protocol/events.d.ts +51 -0
  54. package/dist/protocol/events.d.ts.map +1 -0
  55. package/dist/protocol/events.js +87 -0
  56. package/dist/protocol/events.js.map +1 -0
  57. package/dist/qr.d.ts +15 -0
  58. package/dist/qr.d.ts.map +1 -0
  59. package/dist/qr.js +52 -0
  60. package/dist/qr.js.map +1 -0
  61. package/dist/quickstart.d.ts +167 -0
  62. package/dist/quickstart.d.ts.map +1 -0
  63. package/dist/quickstart.js +482 -0
  64. package/dist/quickstart.js.map +1 -0
  65. package/dist/quota.d.ts +41 -0
  66. package/dist/quota.d.ts.map +1 -0
  67. package/dist/quota.js +84 -0
  68. package/dist/quota.js.map +1 -0
  69. package/dist/runtime/agent.d.ts +100 -0
  70. package/dist/runtime/agent.d.ts.map +1 -0
  71. package/dist/runtime/agent.js +260 -0
  72. package/dist/runtime/agent.js.map +1 -0
  73. package/dist/runtime/executor.d.ts +42 -0
  74. package/dist/runtime/executor.d.ts.map +1 -0
  75. package/dist/runtime/executor.js +87 -0
  76. package/dist/runtime/executor.js.map +1 -0
  77. package/dist/runtime/fairness.d.ts +25 -0
  78. package/dist/runtime/fairness.d.ts.map +1 -0
  79. package/dist/runtime/fairness.js +79 -0
  80. package/dist/runtime/fairness.js.map +1 -0
  81. package/dist/runtime/node.d.ts +91 -0
  82. package/dist/runtime/node.d.ts.map +1 -0
  83. package/dist/runtime/node.js +203 -0
  84. package/dist/runtime/node.js.map +1 -0
  85. package/dist/runtime/openchamber-http.d.ts +27 -0
  86. package/dist/runtime/openchamber-http.d.ts.map +1 -0
  87. package/dist/runtime/openchamber-http.js +118 -0
  88. package/dist/runtime/openchamber-http.js.map +1 -0
  89. package/dist/runtime/openchamber.d.ts +38 -0
  90. package/dist/runtime/openchamber.d.ts.map +1 -0
  91. package/dist/runtime/openchamber.js +43 -0
  92. package/dist/runtime/openchamber.js.map +1 -0
  93. package/dist/runtime/planner.d.ts +23 -0
  94. package/dist/runtime/planner.d.ts.map +1 -0
  95. package/dist/runtime/planner.js +17 -0
  96. package/dist/runtime/planner.js.map +1 -0
  97. package/dist/runtime/worker.d.ts +96 -0
  98. package/dist/runtime/worker.d.ts.map +1 -0
  99. package/dist/runtime/worker.js +296 -0
  100. package/dist/runtime/worker.js.map +1 -0
  101. package/dist/security.d.ts +462 -0
  102. package/dist/security.d.ts.map +1 -0
  103. package/dist/security.js +1197 -0
  104. package/dist/security.js.map +1 -0
  105. package/dist/store/daemon-store.d.ts +33 -0
  106. package/dist/store/daemon-store.d.ts.map +1 -0
  107. package/dist/store/daemon-store.js +115 -0
  108. package/dist/store/daemon-store.js.map +1 -0
  109. package/dist/store/file-store.d.ts +22 -0
  110. package/dist/store/file-store.d.ts.map +1 -0
  111. package/dist/store/file-store.js +63 -0
  112. package/dist/store/file-store.js.map +1 -0
  113. package/dist/store/mebular-store.d.ts +22 -0
  114. package/dist/store/mebular-store.d.ts.map +1 -0
  115. package/dist/store/mebular-store.js +53 -0
  116. package/dist/store/mebular-store.js.map +1 -0
  117. package/dist/store/message-store.d.ts +26 -0
  118. package/dist/store/message-store.d.ts.map +1 -0
  119. package/dist/store/message-store.js +51 -0
  120. package/dist/store/message-store.js.map +1 -0
  121. package/dist/surface.d.ts +43 -0
  122. package/dist/surface.d.ts.map +1 -0
  123. package/dist/surface.js +515 -0
  124. package/dist/surface.js.map +1 -0
  125. package/dist/transport/null.d.ts +7 -0
  126. package/dist/transport/null.d.ts.map +1 -0
  127. package/dist/transport/null.js +16 -0
  128. package/dist/transport/null.js.map +1 -0
  129. package/dist/transport/spool.d.ts +12 -0
  130. package/dist/transport/spool.d.ts.map +1 -0
  131. package/dist/transport/spool.js +54 -0
  132. package/dist/transport/spool.js.map +1 -0
  133. package/dist/transport/types.d.ts +21 -0
  134. package/dist/transport/types.d.ts.map +1 -0
  135. package/dist/transport/types.js +6 -0
  136. package/dist/transport/types.js.map +1 -0
  137. package/dist/version.json +6 -0
  138. package/package.json +37 -4
  139. package/protocol/collab.example.json +23 -0
  140. package/protocol/daemon-home.example.json +66 -0
  141. package/protocol/envelope.example.json +12 -0
  142. package/protocol/state-machine.json +14 -0
  143. package/protocol/task-tree.example.json +72 -0
  144. package/src/agentdir.ts +138 -0
  145. package/src/cli.ts +1192 -0
  146. package/src/collab/chatter.ts +116 -0
  147. package/src/collab/dag.ts +124 -0
  148. package/src/collab/negotiation.ts +130 -0
  149. package/src/collab/tree.ts +184 -0
  150. package/src/config.ts +286 -0
  151. package/src/index.ts +39 -0
  152. package/src/join.ts +227 -0
  153. package/src/jointoken.ts +609 -0
  154. package/src/model.ts +203 -0
  155. package/src/onboard.ts +601 -0
  156. package/src/protocol/envelope.ts +228 -0
  157. package/src/protocol/events.ts +135 -0
  158. package/src/qr.ts +68 -0
  159. package/src/quickstart.ts +632 -0
  160. package/src/quota.ts +112 -0
  161. package/src/runtime/agent.ts +313 -0
  162. package/src/runtime/executor.ts +114 -0
  163. package/src/runtime/fairness.ts +90 -0
  164. package/src/runtime/node.ts +250 -0
  165. package/src/runtime/openchamber-http.ts +137 -0
  166. package/src/runtime/openchamber.ts +66 -0
  167. package/src/runtime/planner.ts +36 -0
  168. package/src/runtime/worker.ts +354 -0
  169. package/src/security.ts +1660 -0
  170. package/src/store/daemon-store.ts +144 -0
  171. package/src/store/file-store.ts +75 -0
  172. package/src/store/mebular-store.ts +66 -0
  173. package/src/store/message-store.ts +62 -0
  174. package/src/surface.ts +535 -0
  175. package/src/transport/null.ts +20 -0
  176. package/src/transport/spool.ts +62 -0
  177. package/src/transport/types.ts +27 -0
  178. package/README.md +0 -3
@@ -0,0 +1,144 @@
1
+ // W2 B1:fleet 客户端化——任务事件存进**守护的本机 app 接口**(loopback HTTP + token)。
2
+ //
3
+ // daemon 模式(默认于统一上车):node/worker 不再监听 libp2p、不托管 join——网络/身份/信任归守护;
4
+ // fleet 只保留任务语义/agents/配额/执行器/工具体系。embedded 模式仅供测试/CI。
5
+
6
+ import http from 'node:http';
7
+ import https from 'node:https';
8
+ import { readFileSync } from 'node:fs';
9
+ import type { TaskEvent } from '../protocol/events.js';
10
+ import { validateTaskEvent } from '../protocol/events.js';
11
+ import { dedupeEvents } from '../model.js';
12
+ import type { TaskEventStore } from './file-store.js';
13
+
14
+ export interface DaemonStoreOptions {
15
+ /** 守护 app 端点,如 http://127.0.0.1:7331 */
16
+ endpoint: string;
17
+ /** 任务分区(默认 tasks) */
18
+ namespace?: string;
19
+ /** 事件节点类型(默认 task_event) */
20
+ type?: string;
21
+ /** bearer token(内联);与 tokenFile 二选一 */
22
+ token?: string;
23
+ /** token 文件(JSON 数组或 {tokens:[…]}? 这里取纯文本/JSON 点路径,默认整文件文本) */
24
+ tokenFile?: string;
25
+ timeoutMs?: number;
26
+ }
27
+
28
+ interface DaemonReply {
29
+ ok: boolean;
30
+ error?: string;
31
+ count?: number;
32
+ nodes?: Array<{ id: string; type: string; namespace: string; content: unknown }>;
33
+ node?: { id: string; type: string; namespace: string };
34
+ }
35
+
36
+ function readToken(options: DaemonStoreOptions): string | undefined {
37
+ if (options.token !== undefined && options.token.length > 0) return options.token;
38
+ if (options.tokenFile !== undefined) {
39
+ try {
40
+ const raw = readFileSync(options.tokenFile, 'utf-8').trim();
41
+ if (raw.startsWith('{')) {
42
+ const parsed = JSON.parse(raw) as { token?: unknown };
43
+ // 仅接受 `{ token }` 形态;`tokens.json`(多令牌)不猜,返回 undefined(调用方应内联 token)
44
+ return typeof parsed.token === 'string' && parsed.token.length > 0 ? parsed.token : undefined;
45
+ }
46
+ return raw.length > 0 ? raw : undefined;
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ function requestJson<T>(url: string, method: string, token: string | undefined, body: unknown, timeoutMs: number): Promise<{ status: number; json: T }> {
55
+ return new Promise((resolve, reject) => {
56
+ const payload = body === undefined ? undefined : JSON.stringify(body);
57
+ const u = new URL(url);
58
+ const transport = u.protocol === 'https:' ? https : http;
59
+ const headers: Record<string, string> = {};
60
+ if (payload !== undefined) {
61
+ headers['content-type'] = 'application/json';
62
+ headers['content-length'] = String(Buffer.byteLength(payload));
63
+ }
64
+ if (token !== undefined) headers.authorization = `Bearer ${token}`;
65
+ const req = transport.request(u, { method, headers }, (res) => {
66
+ let data = '';
67
+ res.on('data', (c: Buffer) => (data += c.toString('utf-8')));
68
+ res.on('end', () => {
69
+ try {
70
+ resolve({ status: res.statusCode ?? 0, json: JSON.parse(data) as T });
71
+ } catch (error) {
72
+ reject(new Error(`守护响应无法解析:${(error as Error).message}`));
73
+ }
74
+ });
75
+ });
76
+ req.setTimeout(timeoutMs, () => req.destroy(new Error('守护请求超时')));
77
+ req.on('error', reject);
78
+ if (payload !== undefined) req.write(payload);
79
+ req.end();
80
+ });
81
+ }
82
+
83
+ /**
84
+ * 走守护 app 接口的任务事件存储。**无本地 Mebular、无 libp2p、无身份处理**。
85
+ * 幂等由 `eventId` 去重保证(与 `MebularTaskEventStore` 同一语义)。
86
+ */
87
+ export class DaemonTaskEventStore implements TaskEventStore {
88
+ private readonly endpoint: string;
89
+ private readonly namespace: string;
90
+ private readonly type: string;
91
+ private readonly token: string | undefined;
92
+ private readonly timeoutMs: number;
93
+
94
+ constructor(options: DaemonStoreOptions) {
95
+ if (typeof options.endpoint !== 'string' || options.endpoint.length === 0) {
96
+ throw new Error('DaemonTaskEventStore 需要 endpoint');
97
+ }
98
+ this.endpoint = options.endpoint.replace(/\/+$/, '');
99
+ this.namespace = options.namespace ?? 'tasks';
100
+ this.type = options.type ?? 'task_event';
101
+ this.token = readToken(options);
102
+ this.timeoutMs = options.timeoutMs ?? 15_000;
103
+ }
104
+
105
+ private async list(): Promise<DaemonReply> {
106
+ const url = `${this.endpoint}/app/nodes?namespace=${encodeURIComponent(this.namespace)}&type=${encodeURIComponent(this.type)}`;
107
+ const { status, json } = await requestJson<DaemonReply>(url, 'GET', this.token, undefined, this.timeoutMs);
108
+ if (status !== 200 || json.ok !== true) throw new Error(`守护 listNodes 失败(HTTP ${status}):${json.error ?? '未知'}`);
109
+ return json;
110
+ }
111
+
112
+ async all(): Promise<TaskEvent[]> {
113
+ const reply = await this.list();
114
+ const valid: TaskEvent[] = [];
115
+ for (const node of reply.nodes ?? []) {
116
+ if (typeof node.content !== 'object' || node.content === null) continue;
117
+ if (!validateTaskEvent(node.content).ok) continue;
118
+ valid.push(node.content as unknown as TaskEvent);
119
+ }
120
+ return dedupeEvents(valid);
121
+ }
122
+
123
+ async append(event: TaskEvent): Promise<boolean> {
124
+ const existing = await this.all();
125
+ if (existing.some((e) => e.eventId === event.eventId)) return false;
126
+ const { status, json } = await requestJson<DaemonReply>(
127
+ `${this.endpoint}/app/nodes`,
128
+ 'POST',
129
+ this.token,
130
+ { type: this.type, namespace: this.namespace, content: event },
131
+ this.timeoutMs,
132
+ );
133
+ if (status !== 200 || json.ok !== true) throw new Error(`守护 createNode 失败(HTTP ${status}):${json.error ?? '未知'}`);
134
+ return true;
135
+ }
136
+
137
+ async byTask(taskId: string): Promise<TaskEvent[]> {
138
+ return (await this.all()).filter((e) => e.taskId === taskId);
139
+ }
140
+
141
+ async close(): Promise<void> {
142
+ // 连接按需创建;无持久资源。
143
+ }
144
+ }
@@ -0,0 +1,75 @@
1
+ // 任务事件本地存储(M2):JSONL 追加 + `eventId` 去重(幂等落库)。
2
+ //
3
+ // 每个进程用自己的存储路径(验收要求:两进程 storage 独立)。重启后从文件恢复,
4
+ // 支撑「重启韧性/不丢不重」。
5
+
6
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
7
+ import { dirname } from 'node:path';
8
+ import type { TaskEvent } from '../protocol/events.js';
9
+
10
+ export interface TaskEventStore {
11
+ /** 追加事件;`eventId` 已存在则返回 `false`(幂等,不重复落库)。 */
12
+ append(event: TaskEvent): Promise<boolean>;
13
+ all(): Promise<TaskEvent[]>;
14
+ byTask(taskId: string): Promise<TaskEvent[]>;
15
+ close(): Promise<void>;
16
+ }
17
+
18
+ export class FileTaskEventStore implements TaskEventStore {
19
+ private readonly path: string;
20
+ private readonly events: TaskEvent[] = [];
21
+ private readonly seen = new Set<string>();
22
+
23
+ private constructor(path: string) {
24
+ this.path = path;
25
+ }
26
+
27
+ /** 打开(或创建)存储并恢复既有事件。 */
28
+ static async open(path: string): Promise<FileTaskEventStore> {
29
+ const store = new FileTaskEventStore(path);
30
+ await mkdir(dirname(path), { recursive: true });
31
+ let raw = '';
32
+ try {
33
+ raw = await readFile(path, 'utf-8');
34
+ } catch {
35
+ raw = '';
36
+ }
37
+ const lines = raw.split('\n');
38
+ for (let i = 0; i < lines.length; i++) {
39
+ const line = lines[i]!.trim();
40
+ if (line === '') continue;
41
+ try {
42
+ store.push(JSON.parse(line) as TaskEvent);
43
+ } catch {
44
+ // 末尾半行(崩溃)跳过;中间行损坏说明文件被破坏,跳过该行但仍继续(不静默重置)。
45
+ continue;
46
+ }
47
+ }
48
+ return store;
49
+ }
50
+
51
+ private push(event: TaskEvent): void {
52
+ if (this.seen.has(event.eventId)) return;
53
+ this.seen.add(event.eventId);
54
+ this.events.push(event);
55
+ }
56
+
57
+ async append(event: TaskEvent): Promise<boolean> {
58
+ if (this.seen.has(event.eventId)) return false;
59
+ await appendFile(this.path, `${JSON.stringify(event)}\n`, 'utf-8');
60
+ this.push(event);
61
+ return true;
62
+ }
63
+
64
+ async all(): Promise<TaskEvent[]> {
65
+ return [...this.events];
66
+ }
67
+
68
+ async byTask(taskId: string): Promise<TaskEvent[]> {
69
+ return this.events.filter((e) => e.taskId === taskId);
70
+ }
71
+
72
+ async close(): Promise<void> {
73
+ // 无长连接;追加即时落盘。
74
+ }
75
+ }
@@ -0,0 +1,66 @@
1
+ // 任务事件存储(M3):以 `@mebular/core` 的**图示记忆**为载体,任务事件即一类记忆节点。
2
+ //
3
+ // 于是「任务经记忆同步传递」——由 core 的同步(写入即推 + anti-entropy + 变更订阅)负责复制,
4
+ // fleet 只消费 `Mebular.graph` 的**公共 API**(createNode/listNodes),不碰 core 内部。
5
+
6
+ import type { Mebular } from '@mebular/core';
7
+ import type { TaskEvent } from '../protocol/events.js';
8
+ import { validateTaskEvent } from '../protocol/events.js';
9
+ import { dedupeEvents } from '../model.js';
10
+ import type { TaskEventStore } from './file-store.js';
11
+
12
+ export interface MebularTaskEventStoreOptions {
13
+ /** 任务分区(默认 `tasks`) */
14
+ namespace?: string;
15
+ /** 任务事件节点类型(默认 `task_event`) */
16
+ type?: string;
17
+ }
18
+
19
+ /** 用 Mebular 图示记忆承载任务事件;幂等由 `eventId` 去重保证。 */
20
+ export class MebularTaskEventStore implements TaskEventStore {
21
+ private readonly mebular: Mebular;
22
+ private readonly namespace: string;
23
+ private readonly type: string;
24
+
25
+ constructor(mebular: Mebular, options: MebularTaskEventStoreOptions = {}) {
26
+ this.mebular = mebular;
27
+ this.namespace = options.namespace ?? 'tasks';
28
+ this.type = options.type ?? 'task_event';
29
+ }
30
+
31
+ private async events(): Promise<TaskEvent[]> {
32
+ // NodeFilter 无 namespace 字段:按 type 取回后在本层过滤 namespace。
33
+ const nodes = await this.mebular.graph.listNodes({ type: this.type });
34
+ const valid: TaskEvent[] = [];
35
+ for (const node of nodes) {
36
+ if (node.namespace !== this.namespace) continue;
37
+ const candidate = node.content;
38
+ if (typeof candidate !== 'object' || candidate === null) continue;
39
+ if (!validateTaskEvent(candidate).ok) continue; // 忽略非任务事件节点
40
+ valid.push(candidate as unknown as TaskEvent);
41
+ }
42
+ // 与 reducer 同一去重语义(同 id 冲突的确定性裁决),避免 store/reducer 漂移
43
+ return dedupeEvents(valid);
44
+ }
45
+
46
+ async append(event: TaskEvent): Promise<boolean> {
47
+ const existing = await this.events();
48
+ if (existing.some((e) => e.eventId === event.eventId)) return false;
49
+ await this.mebular.graph.createNode(this.type, event as unknown as Record<string, unknown>, [], {
50
+ namespace: this.namespace,
51
+ });
52
+ return true;
53
+ }
54
+
55
+ async all(): Promise<TaskEvent[]> {
56
+ return this.events();
57
+ }
58
+
59
+ async byTask(taskId: string): Promise<TaskEvent[]> {
60
+ return (await this.events()).filter((e) => e.taskId === taskId);
61
+ }
62
+
63
+ async close(): Promise<void> {
64
+ // 存储生命周期由 Mebular 实例管理。
65
+ }
66
+ }
@@ -0,0 +1,62 @@
1
+ // 通用「消息」存储(1d):协商/闲聊消息以**图节点**承载,落在既有 **已授权** 的 tasks 分区内,
2
+ // 用**不同节点类型**区分(`negotiation_message` / `chatter_message`),从而随 core 记忆同步传递
3
+ // ——无需新增分区/授权/同步原语。幂等键 = `messageId`(重复 append 返回 false)。
4
+
5
+ import type { Mebular } from '@mebular/core';
6
+
7
+ export interface MebularMessageStoreOptions<T> {
8
+ /** 节点类型(必填,用于与任务事件区分) */
9
+ type: string;
10
+ /** 分区(默认 `tasks`:复用既有已授权分区) */
11
+ namespace?: string;
12
+ /** 校验(形状非法 → 忽略) */
13
+ validate: (input: unknown) => { ok: boolean };
14
+ /** 幂等键 */
15
+ idOf: (message: T) => string;
16
+ }
17
+
18
+ /** 以 Mebular 图示记忆承载的幂等消息存储(协商/闲聊)。 */
19
+ export class MebularMessageStore<T> {
20
+ private readonly mebular: Mebular;
21
+ private readonly namespace: string;
22
+ private readonly type: string;
23
+ private readonly validate: (input: unknown) => { ok: boolean };
24
+ private readonly idOf: (message: T) => string;
25
+
26
+ constructor(mebular: Mebular, options: MebularMessageStoreOptions<T>) {
27
+ this.mebular = mebular;
28
+ this.namespace = options.namespace ?? 'tasks';
29
+ this.type = options.type;
30
+ this.validate = options.validate;
31
+ this.idOf = options.idOf;
32
+ }
33
+
34
+ async all(): Promise<T[]> {
35
+ const nodes = await this.mebular.graph.listNodes({ type: this.type });
36
+ const valid: T[] = [];
37
+ for (const node of nodes) {
38
+ if (node.namespace !== this.namespace) continue;
39
+ const candidate = node.content;
40
+ if (typeof candidate !== 'object' || candidate === null) continue;
41
+ if (!this.validate(candidate).ok) continue;
42
+ valid.push(candidate as unknown as T);
43
+ }
44
+ // 幂等去重(同 messageId 确定取首见;去重后按 id 字典序 → 顺序无关)
45
+ const byId = new Map<string, T>();
46
+ for (const message of valid) if (!byId.has(this.idOf(message))) byId.set(this.idOf(message), message);
47
+ return [...byId.values()].sort((a, b) => (this.idOf(a) < this.idOf(b) ? -1 : this.idOf(a) > this.idOf(b) ? 1 : 0));
48
+ }
49
+
50
+ async append(message: T): Promise<boolean> {
51
+ const id = this.idOf(message);
52
+ if ((await this.all()).some((m) => this.idOf(m) === id)) return false;
53
+ await this.mebular.graph.createNode(this.type, message as unknown as Record<string, unknown>, [], {
54
+ namespace: this.namespace,
55
+ });
56
+ return true;
57
+ }
58
+
59
+ async close(): Promise<void> {
60
+ // 生命周期由 Mebular 管理
61
+ }
62
+ }