重构世界模拟器为模块化配方架构,完善创作编排、会话运行时与 Web UI,并清理过时技能。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-30 00:39:32 +08:00
co-authored by Cursor
parent 2b74c30d36
commit e670a5129c
167 changed files with 22955 additions and 5659 deletions
+33 -17
View File
@@ -10,8 +10,10 @@ export class Blackboard {
private items = new Map<string, BlackboardItem>();
private writeSeq = 0;
listTagIndex(): BlackboardTagIndex[] {
return [...this.items.values()]
listTagIndex(options?: { includeArchived?: boolean }): BlackboardTagIndex[] {
const includeArchived = options?.includeArchived === true;
return latestItemsByTag([...this.items.values()])
.filter((item) => includeArchived || item.metadata?.role !== "archived")
.map(({ id, tag, source, scope, updatedAt }) => ({
id,
tag,
@@ -43,34 +45,38 @@ export class Blackboard {
queryByPatterns(
patterns: string[],
merge: "latest" | "concat" = "latest",
options?: { includeArchived?: boolean },
): BlackboardItem[] {
const includeArchived = options?.includeArchived === true;
const allItems = [...this.items.values()];
if (patterns.some(isFullAccessPattern)) {
return [...this.items.values()].sort((a, b) =>
a.updatedAt.localeCompare(b.updatedAt),
);
const latestByTag = latestItemsByTag(allItems);
return latestByTag
.filter((item) => includeArchived || item.metadata?.role !== "archived")
.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
}
const result: BlackboardItem[] = [];
for (const pattern of patterns) {
const matched = [...this.items.values()]
.filter((item) => tagMatchesPattern(item.tag, pattern))
.sort(compareItemsByRecency);
const matchedLatest = latestItemsByTag(
allItems.filter((item) => tagMatchesPattern(item.tag, pattern)),
).filter((item) => includeArchived || item.metadata?.role !== "archived");
if (matched.length === 0) continue;
if (matchedLatest.length === 0) continue;
if (merge === "concat" && matched.length > 1) {
if (merge === "concat" && matchedLatest.length > 1) {
const ordered = matchedLatest.sort((a, b) =>
a.updatedAt.localeCompare(b.updatedAt),
);
result.push({
...matched[0],
...ordered[ordered.length - 1],
id: `merged:${pattern}`,
content: matched
.slice()
.reverse()
.map((m) => m.content)
.join("\n\n---\n\n"),
content: ordered.map((m) => m.content).join("\n\n---\n\n"),
});
} else {
result.push(matched[0]);
result.push(matchedLatest.sort(compareItemsByRecency)[0]);
}
}
@@ -115,3 +121,13 @@ export class Blackboard {
function compareItemsByRecency(a: BlackboardItem, b: BlackboardItem): number {
return b.updatedAt.localeCompare(a.updatedAt);
}
/** 每个 tag 只保留最新一条 */
function latestItemsByTag(items: BlackboardItem[]): BlackboardItem[] {
const byTag = new Map<string, BlackboardItem>();
for (const item of items) {
const prev = byTag.get(item.tag);
if (!prev || item.updatedAt > prev.updatedAt) byTag.set(item.tag, item);
}
return [...byTag.values()];
}
+141
View File
@@ -0,0 +1,141 @@
/**
* 表字段格:扁平 rows + 每格 rev,防止用户手改被 worker 覆盖。
*/
export type TableCellSource = `user` | `system` | `worker:${string}`;
export type TableCell = {
key: string;
value: unknown;
rev: number;
updatedAt: string;
source: TableCellSource | string;
visibility?: "visible" | "hidden";
note?: string;
};
export type TableDoc = {
rows: TableCell[];
};
export function parseTableDoc(raw: string | undefined | null): TableDoc | null {
if (!raw?.trim()) return null;
try {
const doc = JSON.parse(raw) as unknown;
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
const rowsRaw = (doc as { rows?: unknown }).rows;
if (!Array.isArray(rowsRaw)) return null;
const rows: TableCell[] = [];
for (const item of rowsRaw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const key = typeof row.key === "string" ? row.key.trim() : "";
if (!key) continue;
const rev = typeof row.rev === "number" && row.rev >= 1 ? row.rev : 1;
rows.push({
key,
value: row.value,
rev,
updatedAt:
typeof row.updatedAt === "string" && row.updatedAt
? row.updatedAt
: new Date().toISOString(),
source:
typeof row.source === "string" && row.source
? row.source
: "system",
visibility:
row.visibility === "hidden" || row.visibility === "visible"
? row.visibility
: "visible",
note: typeof row.note === "string" ? row.note : undefined,
});
}
return { rows };
} catch {
return null;
}
}
export function stringifyTableDoc(doc: TableDoc): string {
return JSON.stringify(doc, null, 2);
}
/**
* 合并表更新:worker 须带读时的 expectedRev;
* source=user 的格默认不覆盖;rev 不匹配则跳过该格。
*/
export function mergeTableCells(params: {
current: TableDoc | null;
patch: TableDoc;
actor: TableCellSource | string;
/** 若提供,仅当 current.rev === expectedRev[key] 时才写入 */
expectedRev?: Record<string, number>;
}): { doc: TableDoc; applied: string[]; skipped: Array<{ key: string; reason: string }> } {
const byKey = new Map<string, TableCell>();
for (const row of params.current?.rows ?? []) {
byKey.set(row.key, { ...row });
}
const applied: string[] = [];
const skipped: Array<{ key: string; reason: string }> = [];
const now = new Date().toISOString();
for (const patch of params.patch.rows) {
const key = patch.key.trim();
if (!key) continue;
const existing = byKey.get(key);
if (existing?.source === "user" && !String(params.actor).startsWith("user")) {
skipped.push({ key, reason: "user-owned" });
continue;
}
if (params.expectedRev && existing) {
const expected = params.expectedRev[key];
if (expected != null && existing.rev !== expected) {
skipped.push({
key,
reason: `rev-conflict have=${existing.rev} expected=${expected}`,
});
continue;
}
}
const nextRev = existing ? existing.rev + 1 : patch.rev >= 1 ? patch.rev : 1;
byKey.set(key, {
key,
value: patch.value,
rev: nextRev,
updatedAt: now,
source: params.actor,
visibility: patch.visibility ?? existing?.visibility ?? "visible",
note: patch.note ?? existing?.note,
});
applied.push(key);
}
return {
doc: { rows: [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key)) },
applied,
skipped,
};
}
/** 从零创建:全部 source=actor,rev=1 */
export function createTableFromValues(
values: Record<string, unknown>,
actor: TableCellSource | string,
meta?: Record<string, { visibility?: "visible" | "hidden"; note?: string }>,
): TableDoc {
const now = new Date().toISOString();
const rows: TableCell[] = Object.entries(values).map(([key, value]) => ({
key,
value,
rev: 1,
updatedAt: now,
source: actor,
visibility: meta?.[key]?.visibility ?? "visible",
note: meta?.[key]?.note,
}));
return { rows: rows.sort((a, b) => a.key.localeCompare(b.key)) };
}
+275
View File
@@ -0,0 +1,275 @@
/**
* 表边沿副作用:prev 不满足且 now 满足才触发;once 规则记 fired。
* 规则来自 设计.worker集.tables.side_effects。
*/
import type { Blackboard } from "./blackboard.js";
import type { TableDoc } from "./table-cells.js";
export type SideEffectOp = "eq" | "neq" | "gte" | "lte" | "gt" | "lt" | "truthy" | "changed";
export type SideEffectMode = "once" | "edge" | "every_edge";
export type SideEffectAction =
| { type: "write_tag"; tag: string; content: string }
| { type: "replace_tag"; tag: string; content: string }
| { type: "queue_worker"; workerId: string; note?: string };
export type SideEffectRule = {
id: string;
field: string;
op: SideEffectOp;
value?: unknown;
/** once = 边沿 + 记 fired;edge/every_edge = 仅边沿,可反复跨边沿 */
mode: SideEffectMode;
action: SideEffectAction;
};
export type FiredRegistry = Record<string, { at: string; round?: number }>;
export const SIDE_EFFECT_FIRED_TAG = "运行.表副作用.fired";
export function parseSideEffectRules(tables: unknown): SideEffectRule[] {
if (!tables || typeof tables !== "object" || Array.isArray(tables)) return [];
const raw = (tables as { side_effects?: unknown }).side_effects;
if (!Array.isArray(raw)) return [];
const out: SideEffectRule[] = [];
for (const item of raw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const id = typeof row.id === "string" ? row.id.trim() : "";
const field = typeof row.field === "string" ? row.field.trim() : "";
if (!id || !field) continue;
const op = normalizeOp(row.op);
const mode = normalizeMode(row.mode);
const action = parseAction(row.action);
if (!action) continue;
out.push({ id, field, op, value: row.value, mode, action });
}
return out;
}
function normalizeOp(raw: unknown): SideEffectOp {
const s = typeof raw === "string" ? raw.trim().toLowerCase() : "eq";
const allowed: SideEffectOp[] = [
"eq",
"neq",
"gte",
"lte",
"gt",
"lt",
"truthy",
"changed",
];
return (allowed.includes(s as SideEffectOp) ? s : "eq") as SideEffectOp;
}
function normalizeMode(raw: unknown): SideEffectMode {
const s = typeof raw === "string" ? raw.trim().toLowerCase() : "once";
if (s === "edge" || s === "every_edge") return "every_edge";
return "once";
}
function parseAction(raw: unknown): SideEffectAction | null {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
const a = raw as Record<string, unknown>;
const type = typeof a.type === "string" ? a.type.trim() : "";
if (type === "write_tag" || type === "replace_tag") {
const tag = typeof a.tag === "string" ? a.tag.trim() : "";
const content = typeof a.content === "string" ? a.content : "";
if (!tag) return null;
return { type, tag, content };
}
if (type === "queue_worker") {
const workerId =
typeof a.workerId === "string"
? a.workerId.trim()
: typeof a.worker_id === "string"
? a.worker_id.trim()
: "";
if (!workerId) return null;
return {
type: "queue_worker",
workerId,
note: typeof a.note === "string" ? a.note : undefined,
};
}
return null;
}
export function tableValueMap(doc: TableDoc | null | undefined): Map<string, unknown> {
const map = new Map<string, unknown>();
for (const row of doc?.rows ?? []) {
map.set(row.key, row.value);
}
return map;
}
export function conditionSatisfied(
values: Map<string, unknown>,
rule: SideEffectRule,
prevValues?: Map<string, unknown>,
): boolean {
const now = values.get(rule.field);
if (rule.op === "changed") {
if (!prevValues) return false;
return !sameValue(prevValues.get(rule.field), now);
}
return compareOp(now, rule.op, rule.value);
}
function compareOp(actual: unknown, op: SideEffectOp, expected: unknown): boolean {
switch (op) {
case "eq":
return sameValue(actual, expected);
case "neq":
return !sameValue(actual, expected);
case "truthy":
return Boolean(actual);
case "gte":
case "lte":
case "gt":
case "lt": {
const a = toNumber(actual);
const b = toNumber(expected);
if (a == null || b == null) return false;
if (op === "gte") return a >= b;
if (op === "lte") return a <= b;
if (op === "gt") return a > b;
return a < b;
}
case "changed":
return false;
default:
return false;
}
}
function toNumber(v: unknown): number | null {
if (typeof v === "number" && Number.isFinite(v)) return v;
if (typeof v === "string" && v.trim() && !Number.isNaN(Number(v))) {
return Number(v);
}
return null;
}
function sameValue(a: unknown, b: unknown): boolean {
if (a === b) return true;
if (a == null || b == null) return a == null && b == null;
if (typeof a === "number" || typeof b === "number") {
const na = toNumber(a);
const nb = toNumber(b);
return na != null && nb != null && na === nb;
}
return String(a) === String(b);
}
export type SideEffectTrigger = {
rule: SideEffectRule;
reason: "edge";
};
/**
* 对本轮 prev→now 快照算边沿;同 round 同 ruleId 最多一次。
* once:已 fired 则跳过;触发后写入 nextFired。
* every_edge:每次边沿都触发,不记 fired。
*/
export function evaluateSideEffects(params: {
prev: TableDoc | null;
next: TableDoc;
rules: SideEffectRule[];
fired: FiredRegistry;
round?: number;
}): {
triggers: SideEffectTrigger[];
nextFired: FiredRegistry;
queuedWorkers: Array<{ workerId: string; ruleId: string; note?: string }>;
} {
const prevMap = tableValueMap(params.prev);
const nextMap = tableValueMap(params.next);
const nextFired: FiredRegistry = { ...params.fired };
const triggers: SideEffectTrigger[] = [];
const seen = new Set<string>();
const queuedWorkers: Array<{ workerId: string; ruleId: string; note?: string }> =
[];
for (const rule of params.rules) {
if (seen.has(rule.id)) continue;
if (rule.mode === "once" && nextFired[rule.id]) continue;
const was = conditionSatisfied(prevMap, rule, undefined);
const now = conditionSatisfied(nextMap, rule, prevMap);
// 边沿:prev 不满足、now 满足(changed 用 prev/now 值差)
const edge =
rule.op === "changed"
? conditionSatisfied(nextMap, rule, prevMap)
: !was && now;
if (!edge) continue;
seen.add(rule.id);
triggers.push({ rule, reason: "edge" });
if (rule.mode === "once") {
nextFired[rule.id] = {
at: new Date().toISOString(),
round: params.round,
};
}
if (rule.action.type === "queue_worker") {
queuedWorkers.push({
workerId: rule.action.workerId,
ruleId: rule.id,
note: rule.action.note,
});
}
}
return { triggers, nextFired, queuedWorkers };
}
export function parseFiredRegistry(raw: string | undefined | null): FiredRegistry {
if (!raw?.trim()) return {};
try {
const doc = JSON.parse(raw) as unknown;
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return {};
const out: FiredRegistry = {};
for (const [k, v] of Object.entries(doc as Record<string, unknown>)) {
if (!k.trim()) continue;
if (v && typeof v === "object" && !Array.isArray(v)) {
const row = v as Record<string, unknown>;
out[k] = {
at: typeof row.at === "string" ? row.at : new Date().toISOString(),
round: typeof row.round === "number" ? row.round : undefined,
};
} else if (v === true) {
out[k] = { at: new Date().toISOString() };
}
}
return out;
} catch {
return {};
}
}
export function stringifyFiredRegistry(fired: FiredRegistry): string {
return JSON.stringify(fired, null, 2);
}
/** 执行 write_tag / replace_tag;queue_worker 只返回,由上层调度 */
export function applySideEffectTagActions(params: {
blackboard: Blackboard;
triggers: SideEffectTrigger[];
source?: string;
}): { writtenTags: string[] } {
const writtenTags: string[] = [];
const source = params.source ?? "system:table-side-effect";
for (const { rule } of params.triggers) {
const action = rule.action;
if (action.type !== "write_tag" && action.type !== "replace_tag") continue;
params.blackboard.write({
tag: action.tag,
content: action.content,
source,
});
writtenTags.push(action.tag);
}
return { writtenTags };
}