@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.
- package/dist/agentdir.d.ts +43 -0
- package/dist/agentdir.d.ts.map +1 -0
- package/dist/agentdir.js +107 -0
- package/dist/agentdir.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1135 -0
- package/dist/cli.js.map +1 -0
- package/dist/collab/chatter.d.ts +54 -0
- package/dist/collab/chatter.d.ts.map +1 -0
- package/dist/collab/chatter.js +94 -0
- package/dist/collab/chatter.js.map +1 -0
- package/dist/collab/dag.d.ts +27 -0
- package/dist/collab/dag.d.ts.map +1 -0
- package/dist/collab/dag.js +107 -0
- package/dist/collab/dag.js.map +1 -0
- package/dist/collab/negotiation.d.ts +60 -0
- package/dist/collab/negotiation.d.ts.map +1 -0
- package/dist/collab/negotiation.js +90 -0
- package/dist/collab/negotiation.js.map +1 -0
- package/dist/collab/tree.d.ts +39 -0
- package/dist/collab/tree.d.ts.map +1 -0
- package/dist/collab/tree.js +177 -0
- package/dist/collab/tree.js.map +1 -0
- package/dist/config.d.ts +123 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +206 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/join.d.ts +50 -0
- package/dist/join.d.ts.map +1 -0
- package/dist/join.js +167 -0
- package/dist/join.js.map +1 -0
- package/dist/jointoken.d.ts +144 -0
- package/dist/jointoken.d.ts.map +1 -0
- package/dist/jointoken.js +472 -0
- package/dist/jointoken.js.map +1 -0
- package/dist/model.d.ts +64 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +158 -0
- package/dist/model.js.map +1 -0
- package/dist/onboard.d.ts +90 -0
- package/dist/onboard.d.ts.map +1 -0
- package/dist/onboard.js +510 -0
- package/dist/onboard.js.map +1 -0
- package/dist/protocol/envelope.d.ts +94 -0
- package/dist/protocol/envelope.d.ts.map +1 -0
- package/dist/protocol/envelope.js +169 -0
- package/dist/protocol/envelope.js.map +1 -0
- package/dist/protocol/events.d.ts +51 -0
- package/dist/protocol/events.d.ts.map +1 -0
- package/dist/protocol/events.js +87 -0
- package/dist/protocol/events.js.map +1 -0
- package/dist/qr.d.ts +15 -0
- package/dist/qr.d.ts.map +1 -0
- package/dist/qr.js +52 -0
- package/dist/qr.js.map +1 -0
- package/dist/quickstart.d.ts +167 -0
- package/dist/quickstart.d.ts.map +1 -0
- package/dist/quickstart.js +482 -0
- package/dist/quickstart.js.map +1 -0
- package/dist/quota.d.ts +41 -0
- package/dist/quota.d.ts.map +1 -0
- package/dist/quota.js +84 -0
- package/dist/quota.js.map +1 -0
- package/dist/runtime/agent.d.ts +100 -0
- package/dist/runtime/agent.d.ts.map +1 -0
- package/dist/runtime/agent.js +260 -0
- package/dist/runtime/agent.js.map +1 -0
- package/dist/runtime/executor.d.ts +42 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +87 -0
- package/dist/runtime/executor.js.map +1 -0
- package/dist/runtime/fairness.d.ts +25 -0
- package/dist/runtime/fairness.d.ts.map +1 -0
- package/dist/runtime/fairness.js +79 -0
- package/dist/runtime/fairness.js.map +1 -0
- package/dist/runtime/node.d.ts +91 -0
- package/dist/runtime/node.d.ts.map +1 -0
- package/dist/runtime/node.js +203 -0
- package/dist/runtime/node.js.map +1 -0
- package/dist/runtime/openchamber-http.d.ts +27 -0
- package/dist/runtime/openchamber-http.d.ts.map +1 -0
- package/dist/runtime/openchamber-http.js +118 -0
- package/dist/runtime/openchamber-http.js.map +1 -0
- package/dist/runtime/openchamber.d.ts +38 -0
- package/dist/runtime/openchamber.d.ts.map +1 -0
- package/dist/runtime/openchamber.js +43 -0
- package/dist/runtime/openchamber.js.map +1 -0
- package/dist/runtime/planner.d.ts +23 -0
- package/dist/runtime/planner.d.ts.map +1 -0
- package/dist/runtime/planner.js +17 -0
- package/dist/runtime/planner.js.map +1 -0
- package/dist/runtime/worker.d.ts +96 -0
- package/dist/runtime/worker.d.ts.map +1 -0
- package/dist/runtime/worker.js +296 -0
- package/dist/runtime/worker.js.map +1 -0
- package/dist/security.d.ts +462 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +1197 -0
- package/dist/security.js.map +1 -0
- package/dist/store/daemon-store.d.ts +33 -0
- package/dist/store/daemon-store.d.ts.map +1 -0
- package/dist/store/daemon-store.js +115 -0
- package/dist/store/daemon-store.js.map +1 -0
- package/dist/store/file-store.d.ts +22 -0
- package/dist/store/file-store.d.ts.map +1 -0
- package/dist/store/file-store.js +63 -0
- package/dist/store/file-store.js.map +1 -0
- package/dist/store/mebular-store.d.ts +22 -0
- package/dist/store/mebular-store.d.ts.map +1 -0
- package/dist/store/mebular-store.js +53 -0
- package/dist/store/mebular-store.js.map +1 -0
- package/dist/store/message-store.d.ts +26 -0
- package/dist/store/message-store.d.ts.map +1 -0
- package/dist/store/message-store.js +51 -0
- package/dist/store/message-store.js.map +1 -0
- package/dist/surface.d.ts +43 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +515 -0
- package/dist/surface.js.map +1 -0
- package/dist/transport/null.d.ts +7 -0
- package/dist/transport/null.d.ts.map +1 -0
- package/dist/transport/null.js +16 -0
- package/dist/transport/null.js.map +1 -0
- package/dist/transport/spool.d.ts +12 -0
- package/dist/transport/spool.d.ts.map +1 -0
- package/dist/transport/spool.js +54 -0
- package/dist/transport/spool.js.map +1 -0
- package/dist/transport/types.d.ts +21 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +6 -0
- package/dist/transport/types.js.map +1 -0
- package/dist/version.json +6 -0
- package/package.json +37 -4
- package/protocol/collab.example.json +23 -0
- package/protocol/daemon-home.example.json +66 -0
- package/protocol/envelope.example.json +12 -0
- package/protocol/state-machine.json +14 -0
- package/protocol/task-tree.example.json +72 -0
- package/src/agentdir.ts +138 -0
- package/src/cli.ts +1192 -0
- package/src/collab/chatter.ts +116 -0
- package/src/collab/dag.ts +124 -0
- package/src/collab/negotiation.ts +130 -0
- package/src/collab/tree.ts +184 -0
- package/src/config.ts +286 -0
- package/src/index.ts +39 -0
- package/src/join.ts +227 -0
- package/src/jointoken.ts +609 -0
- package/src/model.ts +203 -0
- package/src/onboard.ts +601 -0
- package/src/protocol/envelope.ts +228 -0
- package/src/protocol/events.ts +135 -0
- package/src/qr.ts +68 -0
- package/src/quickstart.ts +632 -0
- package/src/quota.ts +112 -0
- package/src/runtime/agent.ts +313 -0
- package/src/runtime/executor.ts +114 -0
- package/src/runtime/fairness.ts +90 -0
- package/src/runtime/node.ts +250 -0
- package/src/runtime/openchamber-http.ts +137 -0
- package/src/runtime/openchamber.ts +66 -0
- package/src/runtime/planner.ts +36 -0
- package/src/runtime/worker.ts +354 -0
- package/src/security.ts +1660 -0
- package/src/store/daemon-store.ts +144 -0
- package/src/store/file-store.ts +75 -0
- package/src/store/mebular-store.ts +66 -0
- package/src/store/message-store.ts +62 -0
- package/src/surface.ts +535 -0
- package/src/transport/null.ts +20 -0
- package/src/transport/spool.ts +62 -0
- package/src/transport/types.ts +27 -0
- package/README.md +0 -3
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Mebular Fleet —— 任务 envelope 与显式状态机(M0 骨架;M1 扩展为完整模型)。
|
|
2
|
+
//
|
|
3
|
+
// **任务 = 记忆**:任务是一类带 `namespace`(如 `tasks`)的图上记忆,生命周期由**显式状态事件**
|
|
4
|
+
// 驱动;`expiresAt` 只是本机任务板的**软约定**,不参与跨端一致性判定(见 `SEALING.md` 红线:
|
|
5
|
+
// 墙钟不进一致性判定)。本文件**只消费 `@mebular/core` 的公共 API**,不在 core 引入任务语义。
|
|
6
|
+
|
|
7
|
+
/** 协议版本(envelope 线格式版本;改动 = 破坏性,需全端同版本)。 */
|
|
8
|
+
export const FLEET_PROTOCOL_VERSION = 1 as const;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 任务状态(**单调秩**,用于并发/乱序下的确定性收敛):
|
|
12
|
+
* - `queued` → `claimed` → `running` → `done` | `failed`
|
|
13
|
+
* - `done` / `failed` 为**终态**(同秩,平局由 `TERMINAL_PRECEDENCE` 兜底)。
|
|
14
|
+
*/
|
|
15
|
+
export const TASK_STATUSES = ['queued', 'claimed', 'running', 'done', 'failed'] as const;
|
|
16
|
+
export type TaskStatus = (typeof TASK_STATUSES)[number];
|
|
17
|
+
|
|
18
|
+
/** 状态秩:`done`/`failed` 同秩(终态),其余严格递增。 */
|
|
19
|
+
export const STATUS_RANK: Readonly<Record<TaskStatus, number>> = {
|
|
20
|
+
queued: 0,
|
|
21
|
+
claimed: 1,
|
|
22
|
+
running: 2,
|
|
23
|
+
done: 3,
|
|
24
|
+
failed: 3,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** 终态平局裁决:数值大者胜(确定性)。 */
|
|
28
|
+
export const TERMINAL_PRECEDENCE: Readonly<Record<TaskStatus, number>> = {
|
|
29
|
+
queued: 0,
|
|
30
|
+
claimed: 0,
|
|
31
|
+
running: 0,
|
|
32
|
+
failed: 2,
|
|
33
|
+
done: 1,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** 任务树预算(**W1**):随树单调递减;root 声明,子任务 ≤ 父剩余。 */
|
|
37
|
+
export interface TaskBudget {
|
|
38
|
+
/** 允许的最大深度(root 深度 0;链长 = 祖先数) */
|
|
39
|
+
maxDepth: number;
|
|
40
|
+
/** 每个父任务可派生的直接子任务上限 */
|
|
41
|
+
maxChildren: number;
|
|
42
|
+
/** 整个子树的任务总数上限(含 root) */
|
|
43
|
+
maxTasks: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** root 级派发策略:`children-ok`(默认,可派生)| `root-only`(执行者只能干、不得再派)。 */
|
|
47
|
+
export const DISPATCH_POLICIES = ['children-ok', 'root-only'] as const;
|
|
48
|
+
export type TaskDispatch = (typeof DISPATCH_POLICIES)[number];
|
|
49
|
+
|
|
50
|
+
/** root 预算默认值(W1 上界,可由 root 收紧)。 */
|
|
51
|
+
export const DEFAULT_ROOT_BUDGET: Readonly<TaskBudget> = { maxDepth: 8, maxChildren: 16, maxTasks: 256 };
|
|
52
|
+
|
|
53
|
+
/** 预算合法性(非负整数;越界由树不变式判定,这里只做形状)。 */
|
|
54
|
+
export function isTaskBudget(value: unknown): value is TaskBudget {
|
|
55
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
56
|
+
const b = value as Record<string, unknown>;
|
|
57
|
+
return (
|
|
58
|
+
Number.isInteger(b.maxDepth) && (b.maxDepth as number) >= 0 &&
|
|
59
|
+
Number.isInteger(b.maxChildren) && (b.maxChildren as number) >= 0 &&
|
|
60
|
+
Number.isInteger(b.maxTasks) && (b.maxTasks as number) >= 0
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 派发策略守卫。 */
|
|
65
|
+
export function isTaskDispatch(value: unknown): value is TaskDispatch {
|
|
66
|
+
return typeof value === 'string' && (DISPATCH_POLICIES as readonly string[]).includes(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 端标识:设备 + 该设备上的 Agent 名。 */
|
|
70
|
+
export interface FleetEndpoint {
|
|
71
|
+
device: string;
|
|
72
|
+
agent: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 因果追踪:`causedBy` 指向触发本任务/迁移的父任务;`chain` 为祖先链(去重、有序)。 */
|
|
76
|
+
export interface FleetTrace {
|
|
77
|
+
causedBy?: string;
|
|
78
|
+
chain: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 任务 envelope(图上一类记忆的规范化负载)。 */
|
|
82
|
+
export interface TaskEnvelope {
|
|
83
|
+
/** 协议版本 */
|
|
84
|
+
v: number;
|
|
85
|
+
/** 任务唯一 ID(内容寻址前的逻辑 id;由发起方生成) */
|
|
86
|
+
taskId: string;
|
|
87
|
+
/** 发起端 */
|
|
88
|
+
from: FleetEndpoint;
|
|
89
|
+
/** 目标端(`to.agent` 为 `*` 表示该设备任一 Agent 可领取) */
|
|
90
|
+
to: FleetEndpoint;
|
|
91
|
+
/** 意图/指令(人可读;执行语义由执行器解释) */
|
|
92
|
+
intent: string;
|
|
93
|
+
/** 负载引用(如 Blob/记忆节点 id;不在 envelope 内联大对象) */
|
|
94
|
+
payloadRef?: string;
|
|
95
|
+
/** 当前权威状态(显式状态事件驱动) */
|
|
96
|
+
status: TaskStatus;
|
|
97
|
+
/** 已尝试次数(至少一次投递 + 幂等应用;见 M1) */
|
|
98
|
+
attempts: number;
|
|
99
|
+
/** **软约定**:仅影响本机任务板展示;不参与跨端状态判定 */
|
|
100
|
+
expiresAt?: number;
|
|
101
|
+
/** 因果链 */
|
|
102
|
+
trace: FleetTrace;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 校验结果。 */
|
|
106
|
+
export interface EnvelopeValidation {
|
|
107
|
+
ok: boolean;
|
|
108
|
+
errors: string[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
112
|
+
return typeof value === 'string' && value.length > 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 端标识守卫(`{device,agent}` 均为非空字符串)。 */
|
|
116
|
+
export function isFleetEndpoint(value: unknown): value is FleetEndpoint {
|
|
117
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
118
|
+
const e = value as Record<string, unknown>;
|
|
119
|
+
return isNonEmptyString(e.device) && isNonEmptyString(e.agent);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 状态判定。 */
|
|
123
|
+
export function isTaskStatus(value: unknown): value is TaskStatus {
|
|
124
|
+
return typeof value === 'string' && (TASK_STATUSES as readonly string[]).includes(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 终态判定。 */
|
|
128
|
+
export function isTerminal(status: TaskStatus): boolean {
|
|
129
|
+
return status === 'done' || status === 'failed';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 确定性状态裁决:返回 `a` / `b` 中更靠后的那个(收敛用,与到达顺序无关)。
|
|
134
|
+
* 先比 `STATUS_RANK`;同秩(终态)比 `TERMINAL_PRECEDENCE`;再平局取字典序大的状态名。
|
|
135
|
+
*/
|
|
136
|
+
export function resolveStatus(a: TaskStatus, b: TaskStatus): TaskStatus {
|
|
137
|
+
if (STATUS_RANK[a] !== STATUS_RANK[b]) return STATUS_RANK[a] > STATUS_RANK[b] ? a : b;
|
|
138
|
+
if (TERMINAL_PRECEDENCE[a] !== TERMINAL_PRECEDENCE[b]) {
|
|
139
|
+
return TERMINAL_PRECEDENCE[a] > TERMINAL_PRECEDENCE[b] ? a : b;
|
|
140
|
+
}
|
|
141
|
+
return a >= b ? a : b;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 合法迁移表(显式,避免跳级;与 `protocol/state-machine.json` 一致)。
|
|
146
|
+
* 非法迁移必须被拒绝(M1 负例)。
|
|
147
|
+
*/
|
|
148
|
+
export const TASK_TRANSITIONS: Readonly<Record<TaskStatus, readonly TaskStatus[]>> = {
|
|
149
|
+
queued: ['claimed', 'failed'],
|
|
150
|
+
claimed: ['running', 'failed'],
|
|
151
|
+
running: ['done', 'failed'],
|
|
152
|
+
done: [],
|
|
153
|
+
failed: [],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** 状态是否可迁移(`from === to` 视为幂等自迁移)。 */
|
|
157
|
+
export function canTransition(from: TaskStatus, to: TaskStatus): boolean {
|
|
158
|
+
if (from === to) return true;
|
|
159
|
+
return TASK_TRANSITIONS[from].includes(to);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 校验一个未知输入是否为合法 `TaskEnvelope`(不做数值语义裁决)。 */
|
|
163
|
+
export function validateEnvelope(input: unknown): EnvelopeValidation {
|
|
164
|
+
const errors: string[] = [];
|
|
165
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
166
|
+
return { ok: false, errors: ['envelope 必须是对象'] };
|
|
167
|
+
}
|
|
168
|
+
const e = input as Record<string, unknown>;
|
|
169
|
+
if (e.v !== FLEET_PROTOCOL_VERSION) errors.push(`v 必须为 ${FLEET_PROTOCOL_VERSION}`);
|
|
170
|
+
if (!isNonEmptyString(e.taskId)) errors.push('taskId 必须为非空字符串');
|
|
171
|
+
if (!isFleetEndpoint(e.from)) errors.push('from 必须为 {device,agent} 非空字符串');
|
|
172
|
+
if (!isFleetEndpoint(e.to)) errors.push('to 必须为 {device,agent} 非空字符串');
|
|
173
|
+
if (!isNonEmptyString(e.intent)) errors.push('intent 必须为非空字符串');
|
|
174
|
+
if (e.payloadRef !== undefined && !isNonEmptyString(e.payloadRef)) {
|
|
175
|
+
errors.push('payloadRef 若存在须为非空字符串');
|
|
176
|
+
}
|
|
177
|
+
if (!isTaskStatus(e.status)) errors.push(`status 必须为 ${TASK_STATUSES.join('|')}`);
|
|
178
|
+
if (typeof e.attempts !== 'number' || !Number.isInteger(e.attempts) || e.attempts < 0) {
|
|
179
|
+
errors.push('attempts 必须为非负整数');
|
|
180
|
+
}
|
|
181
|
+
if (e.expiresAt !== undefined && (typeof e.expiresAt !== 'number' || !Number.isFinite(e.expiresAt))) {
|
|
182
|
+
errors.push('expiresAt 若存在须为有限数字(软约定,不参与跨端状态)');
|
|
183
|
+
}
|
|
184
|
+
if (typeof e.trace !== 'object' || e.trace === null) {
|
|
185
|
+
errors.push('trace 必须为对象');
|
|
186
|
+
} else {
|
|
187
|
+
const trace = e.trace as Record<string, unknown>;
|
|
188
|
+
if (trace.causedBy !== undefined && !isNonEmptyString(trace.causedBy)) {
|
|
189
|
+
errors.push('trace.causedBy 若存在须为非空字符串');
|
|
190
|
+
}
|
|
191
|
+
if (!Array.isArray(trace.chain) || !trace.chain.every(isNonEmptyString)) {
|
|
192
|
+
errors.push('trace.chain 必须为字符串数组');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { ok: errors.length === 0, errors };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 递归键序规范化(键按字典序,`undefined` 丢弃)→ 同一对象恒得同一字节串。 */
|
|
199
|
+
function canonicalize(input: unknown): unknown {
|
|
200
|
+
if (Array.isArray(input)) return input.map(canonicalize);
|
|
201
|
+
if (typeof input === 'object' && input !== null) {
|
|
202
|
+
const src = input as Record<string, unknown>;
|
|
203
|
+
const out: Record<string, unknown> = {};
|
|
204
|
+
for (const key of Object.keys(src).sort()) {
|
|
205
|
+
if (src[key] !== undefined) out[key] = canonicalize(src[key]);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
return input;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 序列化为**确定序** JSON(同一 envelope → 同一字节串)。 */
|
|
213
|
+
export function serializeEnvelope(envelope: TaskEnvelope): string {
|
|
214
|
+
return JSON.stringify(canonicalize(envelope));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 解析并校验;非法输入抛错(错误信息含全部违规项)。 */
|
|
218
|
+
export function parseEnvelope(json: string): TaskEnvelope {
|
|
219
|
+
let raw: unknown;
|
|
220
|
+
try {
|
|
221
|
+
raw = JSON.parse(json);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw new Error(`envelope JSON 解析失败:${(error as Error).message}`);
|
|
224
|
+
}
|
|
225
|
+
const result = validateEnvelope(raw);
|
|
226
|
+
if (!result.ok) throw new Error(`envelope 非法:${result.errors.join('; ')}`);
|
|
227
|
+
return raw as TaskEnvelope;
|
|
228
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Fleet 任务事件(M1):**显式状态事件**驱动权威生命周期。
|
|
2
|
+
//
|
|
3
|
+
// 事件是任务(一类记忆)上的状态迁移记录。`at` 是**本机墙钟**,仅用于展示与本地排队,
|
|
4
|
+
// **不参与**权威状态判定(见 `SEALING.md`:墙钟不进一致性判定)。收敛只由
|
|
5
|
+
// `(状态秩, 终态平局裁决, eventId)` 决定(见 `model.ts`)。
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
FLEET_PROTOCOL_VERSION,
|
|
9
|
+
isTaskStatus,
|
|
10
|
+
isFleetEndpoint,
|
|
11
|
+
isTaskBudget,
|
|
12
|
+
isTaskDispatch,
|
|
13
|
+
type FleetEndpoint,
|
|
14
|
+
type FleetTrace,
|
|
15
|
+
type TaskBudget,
|
|
16
|
+
type TaskDispatch,
|
|
17
|
+
type TaskStatus,
|
|
18
|
+
} from './envelope.js';
|
|
19
|
+
|
|
20
|
+
/** 事件类型。 */
|
|
21
|
+
export const TASK_EVENT_TYPES = ['created', 'claimed', 'running', 'done', 'failed'] as const;
|
|
22
|
+
export type TaskEventType = (typeof TASK_EVENT_TYPES)[number];
|
|
23
|
+
|
|
24
|
+
/** 事件类型 → 目标权威状态(一一对应)。 */
|
|
25
|
+
export const EVENT_TO_STATUS: Readonly<Record<TaskEventType, TaskStatus>> = {
|
|
26
|
+
created: 'queued',
|
|
27
|
+
claimed: 'claimed',
|
|
28
|
+
running: 'running',
|
|
29
|
+
done: 'done',
|
|
30
|
+
failed: 'failed',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** 任务事件(线格式)。 */
|
|
34
|
+
export interface TaskEvent {
|
|
35
|
+
/** 协议版本 */
|
|
36
|
+
v: number;
|
|
37
|
+
/** 事件唯一 ID(内容寻址或全局唯一;**幂等键**:重复投递按此去重) */
|
|
38
|
+
eventId: string;
|
|
39
|
+
/** 所属任务 */
|
|
40
|
+
taskId: string;
|
|
41
|
+
/** 事件类型 */
|
|
42
|
+
type: TaskEventType;
|
|
43
|
+
/** 发出该事件的端 */
|
|
44
|
+
actor: FleetEndpoint;
|
|
45
|
+
/** 本机墙钟(**advisory**:不参与收敛) */
|
|
46
|
+
at: number;
|
|
47
|
+
/** 迁移后的权威状态(必须与 `type` 一致) */
|
|
48
|
+
toStatus: TaskStatus;
|
|
49
|
+
/** 因果链 */
|
|
50
|
+
trace: FleetTrace;
|
|
51
|
+
/** `created` 事件必填:目标端 */
|
|
52
|
+
to?: FleetEndpoint;
|
|
53
|
+
/** `created` 事件必填:意图 */
|
|
54
|
+
intent?: string;
|
|
55
|
+
/** `created` 事件可选:**软约定**到期(不参与权威判定) */
|
|
56
|
+
expiresAt?: number;
|
|
57
|
+
/** 结果/负载引用(`done` 常带) */
|
|
58
|
+
payloadRef?: string;
|
|
59
|
+
/** 失败原因(`failed` 常带) */
|
|
60
|
+
reason?: string;
|
|
61
|
+
/** **W1**:`created` 可声明子树预算(随树单调递减;缺省 root 用默认/子任务按父推导) */
|
|
62
|
+
budget?: TaskBudget;
|
|
63
|
+
/** **W1**:`created`(root)可声明派发策略;缺省 `children-ok` */
|
|
64
|
+
dispatch?: TaskDispatch;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 事件类型守卫。 */
|
|
68
|
+
export function isTaskEventType(value: unknown): value is TaskEventType {
|
|
69
|
+
return typeof value === 'string' && (TASK_EVENT_TYPES as readonly string[]).includes(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 校验结果。 */
|
|
73
|
+
export interface TaskEventValidation {
|
|
74
|
+
ok: boolean;
|
|
75
|
+
errors: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
79
|
+
return typeof value === 'string' && value.length > 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 校验未知输入是否为合法 `TaskEvent`(不做迁移合法性判定)。 */
|
|
83
|
+
export function validateTaskEvent(input: unknown): TaskEventValidation {
|
|
84
|
+
const errors: string[] = [];
|
|
85
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
86
|
+
return { ok: false, errors: ['event 必须是对象'] };
|
|
87
|
+
}
|
|
88
|
+
const e = input as Record<string, unknown>;
|
|
89
|
+
if (e.v !== FLEET_PROTOCOL_VERSION) errors.push(`v 必须为 ${FLEET_PROTOCOL_VERSION}`);
|
|
90
|
+
if (!isNonEmptyString(e.eventId)) errors.push('eventId 必须为非空字符串');
|
|
91
|
+
if (!isNonEmptyString(e.taskId)) errors.push('taskId 必须为非空字符串');
|
|
92
|
+
if (!isTaskEventType(e.type)) errors.push(`type 必须为 ${TASK_EVENT_TYPES.join('|')}`);
|
|
93
|
+
if (!isFleetEndpoint(e.actor)) errors.push('actor 必须为 {device,agent} 非空字符串');
|
|
94
|
+
if (typeof e.at !== 'number' || !Number.isFinite(e.at)) errors.push('at 必须为有限数字');
|
|
95
|
+
if (!isTaskStatus(e.toStatus)) errors.push('toStatus 必须为合法任务状态');
|
|
96
|
+
else if (isTaskEventType(e.type) && EVENT_TO_STATUS[e.type] !== e.toStatus) {
|
|
97
|
+
errors.push(`toStatus 必须与 type 一致(${e.type} → ${EVENT_TO_STATUS[e.type]})`);
|
|
98
|
+
}
|
|
99
|
+
if (typeof e.trace !== 'object' || e.trace === null) {
|
|
100
|
+
errors.push('trace 必须为对象');
|
|
101
|
+
} else {
|
|
102
|
+
const trace = e.trace as Record<string, unknown>;
|
|
103
|
+
if (trace.causedBy !== undefined && !isNonEmptyString(trace.causedBy)) {
|
|
104
|
+
errors.push('trace.causedBy 若存在须为非空字符串');
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(trace.chain) || !trace.chain.every(isNonEmptyString)) {
|
|
107
|
+
errors.push('trace.chain 必须为字符串数组');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (e.payloadRef !== undefined && !isNonEmptyString(e.payloadRef)) {
|
|
111
|
+
errors.push('payloadRef 若存在须为非空字符串');
|
|
112
|
+
}
|
|
113
|
+
if (e.reason !== undefined && !isNonEmptyString(e.reason)) {
|
|
114
|
+
errors.push('reason 若存在须为非空字符串');
|
|
115
|
+
}
|
|
116
|
+
if (e.type === 'created') {
|
|
117
|
+
if (!isFleetEndpoint(e.to)) errors.push('created 事件的 to 必须为 {device,agent}');
|
|
118
|
+
if (!isNonEmptyString(e.intent)) errors.push('created 事件的 intent 必须为非空字符串');
|
|
119
|
+
}
|
|
120
|
+
if (e.expiresAt !== undefined && (typeof e.expiresAt !== 'number' || !Number.isFinite(e.expiresAt))) {
|
|
121
|
+
errors.push('expiresAt 若存在须为有限数字(advisory)');
|
|
122
|
+
}
|
|
123
|
+
if (e.budget !== undefined && !isTaskBudget(e.budget)) {
|
|
124
|
+
errors.push('budget 若存在须为 {maxDepth,maxChildren,maxTasks} 非负整数');
|
|
125
|
+
}
|
|
126
|
+
if (e.dispatch !== undefined && !isTaskDispatch(e.dispatch)) {
|
|
127
|
+
errors.push('dispatch 若存在须为 children-ok|root-only');
|
|
128
|
+
}
|
|
129
|
+
return { ok: errors.length === 0, errors };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 便于测试/构造的校验快捷键。 */
|
|
133
|
+
export function isTaskEvent(input: unknown): input is TaskEvent {
|
|
134
|
+
return validateTaskEvent(input).ok;
|
|
135
|
+
}
|
package/src/qr.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// C7 · 配对二维码渲染(应用层能力;`qrcode-generator` 为普通运行时依赖)
|
|
2
|
+
//
|
|
3
|
+
// 语义:QR 内容 = **内联令牌文本本身**(不引自定义 scheme);因此任何扫码器/文本通道等价。
|
|
4
|
+
// 降级:编码失败 → 返回 null + 告警(**只给文本,不报错**)——绝不阻断邀请/加入。
|
|
5
|
+
|
|
6
|
+
import qrcode from 'qrcode-generator';
|
|
7
|
+
|
|
8
|
+
export interface QrRenderResult {
|
|
9
|
+
/** 渲染产物(terminal/svg) */
|
|
10
|
+
value: string;
|
|
11
|
+
/** 渲染形态 */
|
|
12
|
+
kind: 'terminal' | 'svg';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface QrOptions {
|
|
16
|
+
/** 非致命渲染告警;调用方回退文本令牌/分组文本 */
|
|
17
|
+
onWarn?: (message: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeQr(text: string) {
|
|
21
|
+
const qr = qrcode(0, 'M');
|
|
22
|
+
qr.addData(text);
|
|
23
|
+
qr.make();
|
|
24
|
+
return qr;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function errorText(error: unknown): string {
|
|
28
|
+
return error instanceof Error ? error.message : String(error);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// qrcode-generator 的 ASCII 画法是「亮模块 = 块字符」,只适合深色终端。
|
|
32
|
+
// 反相后固定 ANSI 黑字白底,使任何主题下都保持标准极性。
|
|
33
|
+
const TERMINAL_INVERT: Record<string, string> = { ' ': '█', '▀': '▄', '▄': '▀', '█': ' ' };
|
|
34
|
+
|
|
35
|
+
function invertTerminalArt(art: string): string {
|
|
36
|
+
const lines = art.split('\n');
|
|
37
|
+
return lines
|
|
38
|
+
.map((line, index) => {
|
|
39
|
+
// QR module count 恒为奇数(17 + 4n),四模块静区使最后一行半行全是静区:
|
|
40
|
+
// 保持白色,避免反相成黑条。
|
|
41
|
+
const rendered = index === lines.length - 1
|
|
42
|
+
? ' '.repeat(line.length)
|
|
43
|
+
: line.replace(/[█▀▄ ]/g, (ch) => TERMINAL_INVERT[ch] ?? ch);
|
|
44
|
+
return `\x1b[30;47m${rendered}\x1b[0m`;
|
|
45
|
+
})
|
|
46
|
+
.join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 终端二维码(ANSI 黑字白底;内容=令牌文本) */
|
|
50
|
+
export async function renderTerminalQr(text: string, options: QrOptions = {}): Promise<QrRenderResult | null> {
|
|
51
|
+
try {
|
|
52
|
+
return { value: invertTerminalArt(makeQr(text).createASCII(1, 4)), kind: 'terminal' };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
options.onWarn?.(`二维码:终端渲染失败(${errorText(error)})`);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** SVG(控制台服务端渲染;前端以 data-uri <img> 展示,避免注入) */
|
|
60
|
+
export async function renderSvgQr(text: string, options: QrOptions = {}): Promise<QrRenderResult | null> {
|
|
61
|
+
try {
|
|
62
|
+
const value = makeQr(text).createSvgTag({ cellSize: 4, margin: 1 });
|
|
63
|
+
return { value, kind: 'svg' };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
options.onWarn?.(`二维码:SVG 渲染失败(${errorText(error)})`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|