soulbridge 0.0.13 → 0.0.15
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 +1 -0
- package/dist/packages/cli/src/cli.js +24 -3
- package/dist/packages/core/src/index.d.ts +6 -1
- package/dist/packages/core/src/index.js +28 -5
- package/dist/packages/core/src/reflect.d.ts +15 -1
- package/dist/packages/core/src/reflect.js +38 -2
- package/dist/packages/core/src/sqlite.js +18 -2
- package/dist/packages/core/src/store.d.ts +6 -0
- package/dist/packages/core/src/types.d.ts +31 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ SQLite(默认) / Postgres(可选)
|
|
|
114
114
|
| [适配器索引](adapters/README.md) | 各宿主适配器与接入契约 |
|
|
115
115
|
| [quality](docs/quality.md) | 质量模型、九条门禁、五条评审清单 |
|
|
116
116
|
| [references](docs/references.md) | 外部资料的去留与过期判断 |
|
|
117
|
+
| [roadmap](docs/roadmap.md) | 下一步做什么、借鉴了什么、明确不做什么 |
|
|
117
118
|
| [runbook](docs/runbook.md) | 发版流程、维护操作、已知待办 |
|
|
118
119
|
|
|
119
120
|
**规则**:架构级改动必须先有 RFC 或 ADR;聊天里达成的不算决定。
|
|
@@ -312,7 +312,9 @@ async function main() {
|
|
|
312
312
|
}
|
|
313
313
|
for (const memory of memories) {
|
|
314
314
|
const preview = memory.content.replace(/\s+/g, ' ').slice(0, 72);
|
|
315
|
-
|
|
315
|
+
const status = memory.status === 'active' ? '' : ` [${memory.status}]`;
|
|
316
|
+
const superseded = memory.supersededBy ? ` -> ${memory.supersededBy}` : '';
|
|
317
|
+
process.stdout.write(`${memory.id} ${memory.type.padEnd(9)} imp=${String(memory.importance).padEnd(2)}${status}${superseded} ${preview}\n`);
|
|
316
318
|
}
|
|
317
319
|
return;
|
|
318
320
|
}
|
|
@@ -324,6 +326,14 @@ async function main() {
|
|
|
324
326
|
out({ ok: soul.forget(id), id });
|
|
325
327
|
return;
|
|
326
328
|
}
|
|
329
|
+
case 'supersede': {
|
|
330
|
+
const [oldId, newId] = positional;
|
|
331
|
+
if (!oldId || !newId)
|
|
332
|
+
throw new Error('usage: soulbridge supersede <old-id> <new-id>');
|
|
333
|
+
const { soul } = open(flags);
|
|
334
|
+
out({ ok: soul.supersede(oldId, newId), oldId, newId });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
327
337
|
case 'corrections': {
|
|
328
338
|
const { soul } = open(flags);
|
|
329
339
|
const rows = soul.corrections(flags.limit ? Number(flags.limit) : 10);
|
|
@@ -344,9 +354,19 @@ async function main() {
|
|
|
344
354
|
const config = ensureConfig();
|
|
345
355
|
const dbPath = resolveDbPath(flags.db);
|
|
346
356
|
const store = openSqlite(dbPath);
|
|
347
|
-
const memories = store.query({
|
|
357
|
+
const memories = store.query({
|
|
358
|
+
soulId: config.soulId,
|
|
359
|
+
userId: config.userId,
|
|
360
|
+
status: ['active', 'superseded'],
|
|
361
|
+
includeExpired: true,
|
|
362
|
+
limit: 100_000,
|
|
363
|
+
});
|
|
348
364
|
const samples = store.recentAffect(config.soulId, config.userId, 0);
|
|
349
365
|
store.close();
|
|
366
|
+
const now = Date.now();
|
|
367
|
+
const activeCount = memories.filter((m) => m.status === 'active' && (m.validUntil === undefined || m.validUntil > now)).length;
|
|
368
|
+
const supersededCount = memories.filter((m) => m.status === 'superseded').length;
|
|
369
|
+
const expiredCount = memories.filter((m) => m.status === 'active' && m.validUntil !== undefined && m.validUntil <= now).length;
|
|
350
370
|
const byType = new Map();
|
|
351
371
|
for (const memory of memories)
|
|
352
372
|
byType.set(memory.type, (byType.get(memory.type) ?? 0) + 1);
|
|
@@ -373,7 +393,7 @@ async function main() {
|
|
|
373
393
|
const lines = [
|
|
374
394
|
`soul ${config.soulId} / ${config.userId}`,
|
|
375
395
|
`db ${dbPath}`,
|
|
376
|
-
`memories ${memories.length} (${typeText})`,
|
|
396
|
+
`memories ${memories.length} (有效 ${activeCount} · 被取代 ${supersededCount} · 已过期 ${expiredCount}) (${typeText})`,
|
|
377
397
|
`affect ${samples.length} samples${samples.length > 0 ? ` · last ${ago(samples[0].at)}` : ''}`,
|
|
378
398
|
plugin
|
|
379
399
|
? `plugin v${plugin.version ?? '?'} · applied ${ago(plugin.appliedAt)} · inject ${plugin.injects ?? 0} · observe ${plugin.observes ?? 0} · fail ${plugin.observeFails ?? 0}`
|
|
@@ -411,6 +431,7 @@ async function main() {
|
|
|
411
431
|
' list [--limit N] 看它记住了什么',
|
|
412
432
|
' corrections [--limit N] 看被纠正过的证据',
|
|
413
433
|
' forget <id> 忘掉一条记忆(墓碑)',
|
|
434
|
+
' supersede <old> <new> 用新记忆取代旧记忆(保留历史)',
|
|
414
435
|
' status 一眼看它是否在工作',
|
|
415
436
|
' stats 看原始状态(JSON)',
|
|
416
437
|
' demo 端到端演示(无需模型)',
|
|
@@ -52,10 +52,15 @@ export declare class Soul {
|
|
|
52
52
|
constructor(options: SoulOptions);
|
|
53
53
|
/** Verb 1: session start. */
|
|
54
54
|
context(request?: Partial<ContextRequest>): ContextResult;
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Audit view: every non-deleted row, including superseded and expired ones.
|
|
57
|
+
* Prompt builders use `context`, which filters both out.
|
|
58
|
+
*/
|
|
56
59
|
list(limit?: number): Memory[];
|
|
57
60
|
/** Tombstone one memory so it never reaches a prompt again. */
|
|
58
61
|
forget(id: string): boolean;
|
|
62
|
+
/** Mark `oldId` replaced by `newId`; neither row is deleted (RFC 0004). */
|
|
63
|
+
supersede(oldId: string, newId: string): boolean;
|
|
59
64
|
/**
|
|
60
65
|
* Record a user correction as evidence (RFC 0003 phase A). Nothing here
|
|
61
66
|
* reaches a prompt; distillation to a strategy is a later phase.
|
|
@@ -19,7 +19,7 @@ import { buildContext } from './context.js';
|
|
|
19
19
|
import { reflect, ruleExtract, nextEmotion } from './reflect.js';
|
|
20
20
|
import { rank, tokenize } from './scoring.js';
|
|
21
21
|
import { analyzeAffect, emptyAffect, lexiconAnalyzer } from './affect/analyze.js';
|
|
22
|
-
import { llmExtractTurn } from './reflect.js';
|
|
22
|
+
import { gateDrafts, llmExtractTurn } from './reflect.js';
|
|
23
23
|
export class Soul {
|
|
24
24
|
store;
|
|
25
25
|
soulId;
|
|
@@ -59,14 +59,27 @@ export class Soul {
|
|
|
59
59
|
maxItems: request.maxItems,
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Audit view: every non-deleted row, including superseded and expired ones.
|
|
64
|
+
* Prompt builders use `context`, which filters both out.
|
|
65
|
+
*/
|
|
63
66
|
list(limit = 50) {
|
|
64
|
-
return this.store.query({
|
|
67
|
+
return this.store.query({
|
|
68
|
+
soulId: this.soulId,
|
|
69
|
+
userId: this.userId,
|
|
70
|
+
status: ['active', 'superseded'],
|
|
71
|
+
includeExpired: true,
|
|
72
|
+
limit,
|
|
73
|
+
});
|
|
65
74
|
}
|
|
66
75
|
/** Tombstone one memory so it never reaches a prompt again. */
|
|
67
76
|
forget(id) {
|
|
68
77
|
return this.store.forget(id);
|
|
69
78
|
}
|
|
79
|
+
/** Mark `oldId` replaced by `newId`; neither row is deleted (RFC 0004). */
|
|
80
|
+
supersede(oldId, newId) {
|
|
81
|
+
return this.store.supersede(oldId, newId);
|
|
82
|
+
}
|
|
70
83
|
/**
|
|
71
84
|
* Record a user correction as evidence (RFC 0003 phase A). Nothing here
|
|
72
85
|
* reaches a prompt; distillation to a strategy is a later phase.
|
|
@@ -129,10 +142,13 @@ export class Soul {
|
|
|
129
142
|
}
|
|
130
143
|
}
|
|
131
144
|
}
|
|
145
|
+
// Deterministic write gates: shape -> semantics -> per-turn budget. Whatever
|
|
146
|
+
// they refuse is reported, so written: 0 always has a reason.
|
|
147
|
+
const gated = gateDrafts(drafts);
|
|
132
148
|
// observe runs every turn; repeating a sentence must not create a second row.
|
|
133
149
|
const seen = new Set(this.store.query({ soulId: this.soulId, userId: this.userId, limit: 10_000 }).map((m) => m.content.trim()));
|
|
134
150
|
let written = 0;
|
|
135
|
-
for (const draft of
|
|
151
|
+
for (const draft of gated.accepted) {
|
|
136
152
|
if (seen.has(draft.content.trim()))
|
|
137
153
|
continue;
|
|
138
154
|
seen.add(draft.content.trim());
|
|
@@ -164,7 +180,14 @@ export class Soul {
|
|
|
164
180
|
}
|
|
165
181
|
const emotion = nextEmotion(this.store.emotion(this.soulId, this.userId), [turn]);
|
|
166
182
|
this.store.saveEmotion(this.soulId, this.userId, emotion, now);
|
|
167
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
written,
|
|
185
|
+
committed: written > 0,
|
|
186
|
+
rejections: gated.rejected,
|
|
187
|
+
emotion,
|
|
188
|
+
affect,
|
|
189
|
+
extractedBy: written > 0 ? extractedBy : 'none',
|
|
190
|
+
};
|
|
168
191
|
}
|
|
169
192
|
/**
|
|
170
193
|
* Explicit form of `observe`: the host decided this is worth keeping.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Emotion, LlmFn, MemoryType, ReflectResult, Turn } from './types.ts';
|
|
1
|
+
import type { DraftRejection, Emotion, LlmFn, MemoryType, ReflectResult, Turn } from './types.ts';
|
|
2
2
|
import type { SoulStore } from './store.ts';
|
|
3
3
|
interface Draft {
|
|
4
4
|
type: MemoryType;
|
|
@@ -30,6 +30,20 @@ export declare const RULES: Rule[];
|
|
|
30
30
|
export declare function isCorrection(text: string): boolean;
|
|
31
31
|
/** Which marker matched, for statistics; null when the turn is not a correction. */
|
|
32
32
|
export declare function correctionRule(text: string): string | null;
|
|
33
|
+
/** Types an extractor may produce; episodic/relationship are host-local. */
|
|
34
|
+
export declare const EXTRACTABLE_TYPES: MemoryType[];
|
|
35
|
+
export declare const MAX_CONTENT_CHARS = 500;
|
|
36
|
+
/** A single turn should not flood the store. */
|
|
37
|
+
export declare const MAX_DRAFTS_PER_TURN = 3;
|
|
38
|
+
/**
|
|
39
|
+
* Deterministic write gates. A draft must pass shape, then semantics, then the
|
|
40
|
+
* per-turn budget; anything refused is reported with the gate that refused it,
|
|
41
|
+
* so a "written: 0" is explainable rather than mysterious.
|
|
42
|
+
*/
|
|
43
|
+
export declare function gateDrafts(drafts: Draft[]): {
|
|
44
|
+
accepted: Draft[];
|
|
45
|
+
rejected: DraftRejection[];
|
|
46
|
+
};
|
|
33
47
|
export declare function ruleExtract(turns: Turn[]): Draft[];
|
|
34
48
|
/** Corrections raise tension and lower passion; an ordinary session lets tension decay. */
|
|
35
49
|
export declare function nextEmotion(current: Emotion, turns: Turn[]): Emotion;
|
|
@@ -86,6 +86,41 @@ export function correctionRule(text) {
|
|
|
86
86
|
}
|
|
87
87
|
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
88
88
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
89
|
+
/** Types an extractor may produce; episodic/relationship are host-local. */
|
|
90
|
+
export const EXTRACTABLE_TYPES = ['strategy', 'trait', 'fact', 'cognition'];
|
|
91
|
+
export const MAX_CONTENT_CHARS = 500;
|
|
92
|
+
/** A single turn should not flood the store. */
|
|
93
|
+
export const MAX_DRAFTS_PER_TURN = 3;
|
|
94
|
+
/**
|
|
95
|
+
* Deterministic write gates. A draft must pass shape, then semantics, then the
|
|
96
|
+
* per-turn budget; anything refused is reported with the gate that refused it,
|
|
97
|
+
* so a "written: 0" is explainable rather than mysterious.
|
|
98
|
+
*/
|
|
99
|
+
export function gateDrafts(drafts) {
|
|
100
|
+
const accepted = [];
|
|
101
|
+
const rejected = [];
|
|
102
|
+
for (const draft of drafts) {
|
|
103
|
+
const content = draft.content.trim();
|
|
104
|
+
if (!EXTRACTABLE_TYPES.includes(draft.type)) {
|
|
105
|
+
rejected.push({ gate: 'shape', reason: `type ${draft.type} is not extractable`, content });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (content.length < 2 || content.length > MAX_CONTENT_CHARS) {
|
|
109
|
+
rejected.push({ gate: 'shape', reason: `length ${content.length} (2..${MAX_CONTENT_CHARS})`, content });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (content.replace(/[\s\p{P}\p{S}]/gu, '').length < 2) {
|
|
113
|
+
rejected.push({ gate: 'semantic', reason: 'no word characters', content });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (accepted.length >= MAX_DRAFTS_PER_TURN) {
|
|
117
|
+
rejected.push({ gate: 'budget', reason: `more than ${MAX_DRAFTS_PER_TURN} drafts in one turn`, content });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
accepted.push({ ...draft, content });
|
|
121
|
+
}
|
|
122
|
+
return { accepted, rejected };
|
|
123
|
+
}
|
|
89
124
|
export function ruleExtract(turns) {
|
|
90
125
|
const drafts = [];
|
|
91
126
|
for (const turn of turns) {
|
|
@@ -149,8 +184,9 @@ export async function reflect(store, options) {
|
|
|
149
184
|
}
|
|
150
185
|
const existing = store.query({ soulId: options.soulId, userId: options.userId });
|
|
151
186
|
const seen = new Set(existing.map((m) => m.content));
|
|
187
|
+
const { accepted, rejected } = gateDrafts(drafts);
|
|
152
188
|
let written = 0;
|
|
153
|
-
for (const draft of
|
|
189
|
+
for (const draft of accepted) {
|
|
154
190
|
if (seen.has(draft.content))
|
|
155
191
|
continue;
|
|
156
192
|
seen.add(draft.content);
|
|
@@ -177,7 +213,7 @@ export async function reflect(store, options) {
|
|
|
177
213
|
emotion = nextEmotion(emotion, options.turns);
|
|
178
214
|
store.saveEmotion(options.soulId, options.userId, emotion);
|
|
179
215
|
}
|
|
180
|
-
return { memories: written, superseded:
|
|
216
|
+
return { memories: written, superseded: null, emotion, committed: written > 0, rejections: rejected };
|
|
181
217
|
}
|
|
182
218
|
/* ------------------------------------------------------------------ *
|
|
183
219
|
* LLM distillation port (optional)
|
|
@@ -236,12 +236,15 @@ export function openSqlite(file) {
|
|
|
236
236
|
query(spec) {
|
|
237
237
|
const statuses = spec.status ?? ['active'];
|
|
238
238
|
const placeholders = statuses.map(() => '?').join(', ');
|
|
239
|
+
const now = spec.now ?? Date.now();
|
|
240
|
+
// Expiry is a filter, not a deletion: audit views can still see the row.
|
|
241
|
+
const expiry = spec.includeExpired ? '' : 'AND (valid_until IS NULL OR valid_until > ?)';
|
|
239
242
|
const rows = db
|
|
240
243
|
.prepare(`SELECT * FROM memories
|
|
241
244
|
WHERE soul_id = ? AND (user_id = ? OR visibility = 'shared')
|
|
242
|
-
AND status IN (${placeholders}) AND deleted_at IS NULL
|
|
245
|
+
AND status IN (${placeholders}) AND deleted_at IS NULL ${expiry}
|
|
243
246
|
ORDER BY created_at DESC LIMIT ?`)
|
|
244
|
-
.all(spec.soulId, spec.userId, ...statuses, spec.limit ?? 500);
|
|
247
|
+
.all(spec.soulId, spec.userId, ...statuses, ...(spec.includeExpired ? [] : [now]), spec.limit ?? 500);
|
|
245
248
|
const memories = rows.map(rowToMemory);
|
|
246
249
|
return spec.types ? memories.filter((m) => spec.types.includes(m.type)) : memories;
|
|
247
250
|
},
|
|
@@ -258,6 +261,19 @@ export function openSqlite(file) {
|
|
|
258
261
|
.run(now, now, id));
|
|
259
262
|
return Number(result.changes ?? 0) > 0;
|
|
260
263
|
},
|
|
264
|
+
/** Mark `oldId` as replaced by `newId`. Neither row is deleted. */
|
|
265
|
+
supersede(oldId, newId, now = Date.now()) {
|
|
266
|
+
const replacement = db
|
|
267
|
+
.prepare('SELECT id FROM memories WHERE id = ? AND deleted_at IS NULL')
|
|
268
|
+
.get(newId);
|
|
269
|
+
if (!replacement)
|
|
270
|
+
return false;
|
|
271
|
+
const result = withRetry(() => db
|
|
272
|
+
.prepare(`UPDATE memories SET status = 'superseded', superseded_by = ?, updated_at = ?
|
|
273
|
+
WHERE id = ? AND status = 'active' AND deleted_at IS NULL`)
|
|
274
|
+
.run(newId, now, oldId));
|
|
275
|
+
return Number(result.changes ?? 0) > 0;
|
|
276
|
+
},
|
|
261
277
|
saveAffect(soulId, userId, signal, at, deviceId) {
|
|
262
278
|
withRetry(() => {
|
|
263
279
|
db.prepare(`INSERT OR IGNORE INTO affect_samples
|
|
@@ -18,6 +18,12 @@ export interface SoulStore {
|
|
|
18
18
|
* merge, the deletion wins by updatedAt). Returns false when no such active row.
|
|
19
19
|
*/
|
|
20
20
|
forget(id: string, now?: number): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Mark `oldId` as replaced by `newId` (status superseded, supersededBy set).
|
|
23
|
+
* Both rows stay; returns false when the replacement is missing or the old row
|
|
24
|
+
* is not active.
|
|
25
|
+
*/
|
|
26
|
+
supersede(oldId: string, newId: string, now?: number): boolean;
|
|
21
27
|
/** Persist one affect sample (payload only; no raw text). */
|
|
22
28
|
saveAffect(soulId: string, userId: string, signal: AffectSignal, at: number, deviceId?: string): void;
|
|
23
29
|
/** Samples newer than `sinceMs`, newest first. */
|
|
@@ -23,8 +23,11 @@ export interface Memory {
|
|
|
23
23
|
keywords: string[];
|
|
24
24
|
createdAt: number;
|
|
25
25
|
lastRecalledAt?: number;
|
|
26
|
+
/** Written on create; no code filters by it yet (roadmap N3). */
|
|
26
27
|
validFrom?: number;
|
|
28
|
+
/** Reserved for expiry filtering; not enforced yet (roadmap N3). */
|
|
27
29
|
validUntil?: number;
|
|
30
|
+
/** Reserved for conflict replacement; not written yet (roadmap N3). */
|
|
28
31
|
supersededBy?: string;
|
|
29
32
|
/** Last-writer-wins clock for cross-device merge (ADR 0021). */
|
|
30
33
|
updatedAt?: number;
|
|
@@ -81,12 +84,6 @@ export interface ContextResult {
|
|
|
81
84
|
directives: string[];
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
|
-
export interface RecallRequest {
|
|
85
|
-
soulId: string;
|
|
86
|
-
userId: string;
|
|
87
|
-
q: string;
|
|
88
|
-
limit?: number;
|
|
89
|
-
}
|
|
90
87
|
export interface ScoredMemory {
|
|
91
88
|
memory: Memory;
|
|
92
89
|
score: number;
|
|
@@ -97,14 +94,41 @@ export interface QuerySpec {
|
|
|
97
94
|
status?: MemoryStatus[];
|
|
98
95
|
types?: MemoryType[];
|
|
99
96
|
limit?: number;
|
|
97
|
+
/** Clock for expiry checks; defaults to now. */
|
|
98
|
+
now?: number;
|
|
99
|
+
/**
|
|
100
|
+
* Audit views (`list`) need to see expired rows; prompt builders must not.
|
|
101
|
+
* Defaults to false, so expiry applies unless a caller asks otherwise.
|
|
102
|
+
*/
|
|
103
|
+
includeExpired?: boolean;
|
|
100
104
|
}
|
|
101
105
|
export interface ReflectResult {
|
|
102
106
|
memories: number;
|
|
103
|
-
|
|
107
|
+
/**
|
|
108
|
+
* How many older memories were replaced by a newer conflicting one.
|
|
109
|
+
*
|
|
110
|
+
ull` means this build does not track supersession yet — it used to report
|
|
111
|
+
* a hardcoded 0, which claimed a capability that did not exist.
|
|
112
|
+
*/
|
|
113
|
+
superseded: number | null;
|
|
104
114
|
emotion: Emotion;
|
|
115
|
+
/** True when at least one memory was durably written by this call. */
|
|
116
|
+
committed: boolean;
|
|
117
|
+
/** Drafts refused before writing, with the gate that refused them. */
|
|
118
|
+
rejections: DraftRejection[];
|
|
119
|
+
}
|
|
120
|
+
export type GateName = 'shape' | 'semantic' | 'budget';
|
|
121
|
+
export interface DraftRejection {
|
|
122
|
+
gate: GateName;
|
|
123
|
+
reason: string;
|
|
124
|
+
content: string;
|
|
105
125
|
}
|
|
106
126
|
export interface ObserveResult {
|
|
107
127
|
written: number;
|
|
128
|
+
/** True when at least one memory was durably written by this call. */
|
|
129
|
+
committed: boolean;
|
|
130
|
+
/** Drafts refused before writing, with the gate that refused them. */
|
|
131
|
+
rejections: DraftRejection[];
|
|
108
132
|
emotion: Emotion;
|
|
109
133
|
/** Analysis of the user turn. Empty signal for assistant turns or on failure. */
|
|
110
134
|
affect: AffectSignal;
|
|
@@ -166,12 +190,3 @@ export interface Correction {
|
|
|
166
190
|
rule?: string;
|
|
167
191
|
deviceId?: string;
|
|
168
192
|
}
|
|
169
|
-
export interface CorrectionInput {
|
|
170
|
-
soulId: string;
|
|
171
|
-
userId: string;
|
|
172
|
-
userText: string;
|
|
173
|
-
assistantText?: string;
|
|
174
|
-
rule?: string;
|
|
175
|
-
deviceId?: string;
|
|
176
|
-
at?: number;
|
|
177
|
-
}
|