@danceiny/gotry 0.0.1-rc.8 → 0.0.1-rc.9
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 +6 -4
- package/cordis.gotry-patch.yml +7 -0
- package/dist/capabilities/flyai.js +131 -0
- package/dist/capabilities/session/action-cache.js +120 -0
- package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
- package/dist/capabilities/session/adapters/meituan-local.js +73 -0
- package/dist/capabilities/session/extract.js +40 -0
- package/dist/capabilities/session/read-guard.js +56 -0
- package/dist/capabilities/session/transport.js +95 -0
- package/dist/capabilities/session-search.js +95 -0
- package/dist/scripts/action-cache-tests.js +107 -0
- package/dist/scripts/async-collect.js +26 -7
- package/dist/scripts/companion-tests.js +112 -0
- package/dist/scripts/ledger-tests.js +344 -0
- package/dist/scripts/ledger-workflow-crash.js +42 -0
- package/dist/scripts/memory-decay-tests.js +83 -0
- package/dist/scripts/memory-metrics.js +6 -20
- package/dist/scripts/nudge-digest.js +4 -14
- package/dist/scripts/session-attach-diagnose.js +31 -0
- package/dist/scripts/session-attach-poc.js +65 -0
- package/dist/scripts/session-attach-wait.js +41 -0
- package/dist/scripts/session-extract-tests.js +81 -0
- package/dist/scripts/session-login.js +48 -0
- package/dist/scripts/session-tests.js +162 -0
- package/dist/scripts/smoke.js +41 -1
- package/dist/scripts/state-cli-tests.js +136 -0
- package/dist/scripts/state-cli.js +234 -0
- package/dist/scripts/travel-timeline-tests.js +123 -0
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/src/companions.js +112 -0
- package/dist/src/index.js +346 -117
- package/dist/src/loop.js +52 -15
- package/dist/src/memory-decay.js +31 -0
- package/dist/src/state-ledger.js +848 -0
- package/dist/src/travel-timeline.js +78 -0
- package/dist/src/unified.js +3 -1
- package/package.json +1 -1
- package/ts/package.json +3 -0
- package/ts/src/index.ts +216 -83
- package/ts/src/loop.ts +57 -17
- package/ts/src/unified.ts +6 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { ensureLedger, openLedgerIfExists } from '../src/state-ledger.js';
|
|
4
|
+
import { collectDeepPlanning, makeJournaledSolvePort, settleAsyncTicket } from '../src/loop.js';
|
|
5
|
+
import { solveUnified } from '../src/unified.js';
|
|
6
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
7
|
+
function rootOf(args) {
|
|
8
|
+
const i = args.indexOf('--state-root');
|
|
9
|
+
if (i >= 0 && args[i + 1]) return args[i + 1];
|
|
10
|
+
const first = args.find((a)=>!a.startsWith('--') && !/^[\d]+$/.test(a));
|
|
11
|
+
return first ?? '.';
|
|
12
|
+
}
|
|
13
|
+
function arg(name, fallback) {
|
|
14
|
+
const i = process.argv.indexOf(name);
|
|
15
|
+
return i >= 0 ? process.argv[i + 1] : fallback;
|
|
16
|
+
}
|
|
17
|
+
function atomicWrite(path, text) {
|
|
18
|
+
const tmp = `${path}.tmp`;
|
|
19
|
+
writeFileSync(tmp, text, 'utf-8');
|
|
20
|
+
renameSync(tmp, path);
|
|
21
|
+
}
|
|
22
|
+
const HELP = '用法见文件头注释(npx tsx scripts/state-cli.ts <cmd> [root] ...)';
|
|
23
|
+
switch(cmd){
|
|
24
|
+
case 'migrate':
|
|
25
|
+
{
|
|
26
|
+
const root = rootOf(rest);
|
|
27
|
+
const before = openLedgerIfExists(root)?.countEvents() ?? 0;
|
|
28
|
+
const ledger = ensureLedger(root);
|
|
29
|
+
const backupDir = join(root, 'gotry-state', 'pre-ledger-backup');
|
|
30
|
+
console.log(`账本就绪:${ledger.dbPath}(events ${before} → ${ledger.countEvents()};存在旧文件时已快照至 ${backupDir}/)`);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case 'export':
|
|
34
|
+
{
|
|
35
|
+
const root = rootOf(rest);
|
|
36
|
+
const ledger = ensureLedger(root);
|
|
37
|
+
const dir = join(root === '.' ? process.cwd() : root, 'gotry-state');
|
|
38
|
+
mkdirSync(dir, {
|
|
39
|
+
recursive: true
|
|
40
|
+
});
|
|
41
|
+
const motivation = ledger.readMotivation();
|
|
42
|
+
if (motivation) atomicWrite(join(dir, 'motivation-profile.json'), JSON.stringify(motivation, null, 2));
|
|
43
|
+
const pool = ledger.readWishPool();
|
|
44
|
+
if (pool.length) atomicWrite(join(dir, 'wish-pool.json'), JSON.stringify(pool, null, 2));
|
|
45
|
+
const companions = ledger.readCompanions();
|
|
46
|
+
if (companions.length) atomicWrite(join(dir, 'companions.json'), JSON.stringify(companions, null, 2));
|
|
47
|
+
const utility = ledger.readUtilityEvents();
|
|
48
|
+
if (utility.length) atomicWrite(join(dir, 'memory-utility.jsonl'), utility.map((e)=>JSON.stringify(e)).join('\n') + '\n');
|
|
49
|
+
const trips = ledger.readTrips();
|
|
50
|
+
if (trips.length) atomicWrite(join(dir, 'trips.jsonl'), trips.map((e)=>JSON.stringify(e)).join('\n') + '\n');
|
|
51
|
+
console.log(`视图已导出(单向,DB→文件;红线 6):${dir}/`);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'log':
|
|
55
|
+
{
|
|
56
|
+
const root = rootOf(rest);
|
|
57
|
+
const limit = Number(arg('--limit', '20'));
|
|
58
|
+
const ledger = openLedgerIfExists(root);
|
|
59
|
+
if (!ledger) {
|
|
60
|
+
console.log('(无账本——未迁移 root,旧文件形态)');
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
for (const e of ledger.readEvents(undefined, limit)){
|
|
64
|
+
console.log(`${String(e.seq).padStart(5)} ${e.ts} ${e.actor.padEnd(28)} ${e.kind.padEnd(24)} ${e.subject_id}${e.idem_key ? ` #${e.idem_key}` : ''}`);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'stats':
|
|
69
|
+
{
|
|
70
|
+
const root = rootOf(rest);
|
|
71
|
+
const ledger = openLedgerIfExists(root);
|
|
72
|
+
if (!ledger) {
|
|
73
|
+
console.log('(无账本——未迁移 root)');
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
console.log(`events=${ledger.countEvents()} wishes=${ledger.readWishPool().length} companions=${ledger.readCompanions().length} trips=${ledger.readTrips().length} utility=${ledger.readUtilityEvents().length} pendingRuns=${ledger.pendingWorkflowRuns().length} pendingWrites=${ledger.listPendingWrites().length}`);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'rebuild':
|
|
80
|
+
case 'rewind':
|
|
81
|
+
{
|
|
82
|
+
const root = rootOf(rest);
|
|
83
|
+
const ledger = openLedgerIfExists(root);
|
|
84
|
+
if (!ledger) {
|
|
85
|
+
console.error('无账本');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const toSeqRaw = rest.find((a)=>/^\d+$/.test(a));
|
|
89
|
+
const r = ledger.rebuildProjections(toSeqRaw ? Number(toSeqRaw) : undefined);
|
|
90
|
+
console.log(`fold 重建完成${toSeqRaw ? `(至 seq ${toSeqRaw};rebuild 无参即回最新)` : '(全量)'}:重放 ${r.events} 事件 → 愿望 ${r.wishes} / 同行人 ${r.companions}`);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'forget':
|
|
94
|
+
{
|
|
95
|
+
const root = rootOf(rest);
|
|
96
|
+
const ledger = openLedgerIfExists(root);
|
|
97
|
+
if (!ledger) {
|
|
98
|
+
console.error('无账本');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const subject = rest.find((a)=>!a.startsWith('--') && a !== root);
|
|
102
|
+
const id = rest[rest.indexOf(subject) + 1];
|
|
103
|
+
if (!subject || id === undefined) {
|
|
104
|
+
console.error(HELP);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const map = {
|
|
108
|
+
wish: {
|
|
109
|
+
kinds: [
|
|
110
|
+
'wish.imported',
|
|
111
|
+
'wish.added',
|
|
112
|
+
'wish.updated',
|
|
113
|
+
'memory_utility.event'
|
|
114
|
+
],
|
|
115
|
+
subjectId: id
|
|
116
|
+
},
|
|
117
|
+
companion: {
|
|
118
|
+
kinds: [
|
|
119
|
+
'companion.imported',
|
|
120
|
+
'companion.saved'
|
|
121
|
+
],
|
|
122
|
+
subjectId: id
|
|
123
|
+
},
|
|
124
|
+
motivation: {
|
|
125
|
+
kinds: [
|
|
126
|
+
'motivation.imported',
|
|
127
|
+
'motivation.patch'
|
|
128
|
+
],
|
|
129
|
+
subjectId: 'motivation'
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const spec = map[subject];
|
|
133
|
+
if (!spec) {
|
|
134
|
+
console.error(`未知主体 ${subject}(wish|companion|motivation)`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const r = ledger.forgetSubject([
|
|
138
|
+
spec
|
|
139
|
+
]);
|
|
140
|
+
console.log(`已物理硬删 ${r.deleted} 条事件并重建投影(审计一行留痕;红线 6「可删除」)`);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'tick':
|
|
144
|
+
{
|
|
145
|
+
const root = rootOf(rest);
|
|
146
|
+
const ledger = openLedgerIfExists(root);
|
|
147
|
+
if (!ledger) {
|
|
148
|
+
console.log('(无账本,无待办)');
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
const pending = ledger.pendingWorkflowRuns();
|
|
152
|
+
if (pending.length === 0) {
|
|
153
|
+
console.log('(无 pending 工单)');
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
for (const p of pending){
|
|
157
|
+
const run = ledger.getWorkflowRun(p.id);
|
|
158
|
+
const ticket = JSON.parse(run.ticket_json);
|
|
159
|
+
const state = JSON.parse(run.state_json);
|
|
160
|
+
const solve = makeJournaledSolvePort(ledger, p.id, solveUnified);
|
|
161
|
+
const { reply } = await collectDeepPlanning(state, ticket, solve);
|
|
162
|
+
await settleAsyncTicket(p.id, reply, root);
|
|
163
|
+
console.log(`工单 ${p.id} 已回收(durable 恢复语义:done 步骤零重执行)`);
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'whatif':
|
|
168
|
+
{
|
|
169
|
+
const root = rootOf(rest);
|
|
170
|
+
const ledger = openLedgerIfExists(root);
|
|
171
|
+
if (!ledger) {
|
|
172
|
+
console.error('无账本');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
const dest = rest.find((a)=>a !== root && !a.startsWith('--'));
|
|
176
|
+
if (!dest) {
|
|
177
|
+
console.error(HELP);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
mkdirSync(dirname(dest), {
|
|
181
|
+
recursive: true
|
|
182
|
+
});
|
|
183
|
+
ledger.forkWhatIf(dest);
|
|
184
|
+
console.log(`what-if 分叉已生成:${dest}(预演在副本,正本零改动)`);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case 'pw-list':
|
|
188
|
+
{
|
|
189
|
+
const root = rootOf(rest);
|
|
190
|
+
const ledger = openLedgerIfExists(root);
|
|
191
|
+
if (!ledger) {
|
|
192
|
+
console.log('(无账本)');
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
for (const w of ledger.listPendingWrites()){
|
|
196
|
+
console.log(`${w.status.padEnd(12)} ${w.idem_key} seam=${w.seam}${w.receipt ? ` receipt=${w.receipt}` : ''}`);
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case 'pw-request':
|
|
201
|
+
case 'pw-confirm':
|
|
202
|
+
case 'pw-compensate':
|
|
203
|
+
{
|
|
204
|
+
const root = rootOf(rest);
|
|
205
|
+
const ledger = ensureLedger(root);
|
|
206
|
+
const positional = rest.filter((a)=>a !== root && !a.startsWith('--'));
|
|
207
|
+
if (positional.length < (cmd === 'pw-request' ? 3 : 2)) {
|
|
208
|
+
console.error(HELP);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
if (cmd === 'pw-request') {
|
|
212
|
+
const r = ledger.requestPendingWrite({
|
|
213
|
+
idemKey: positional[0],
|
|
214
|
+
seam: positional[1],
|
|
215
|
+
payload: JSON.parse(positional[2])
|
|
216
|
+
});
|
|
217
|
+
console.log(`登记:${JSON.stringify(r)}(L2:只登记不执行)`);
|
|
218
|
+
} else if (cmd === 'pw-confirm') {
|
|
219
|
+
const r = ledger.confirmPendingWrite(positional[0], positional[1]);
|
|
220
|
+
console.log(`确认:${JSON.stringify(r)}`);
|
|
221
|
+
} else {
|
|
222
|
+
const r = ledger.compensatePendingWrite(positional[0], positional[1]);
|
|
223
|
+
console.log(`补偿:${JSON.stringify(r)}`);
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
default:
|
|
228
|
+
console.error(cmd ? `未知命令 ${cmd}` : HELP);
|
|
229
|
+
console.error(HELP);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/state-cli.ts
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { appendTrip, makeTripId, projectTimeline, timelineGapsForVerified, resolveTimelineDate } from '../src/travel-timeline.js';
|
|
3
|
+
import { buildTimeAnchor } from '../src/time-anchor.js';
|
|
4
|
+
const anchor = buildTimeAnchor(new Date(2026, 7, 28, 12));
|
|
5
|
+
let n = 0;
|
|
6
|
+
function pass(name, body) {
|
|
7
|
+
body();
|
|
8
|
+
console.log(` ${++n}. ${name} OK`);
|
|
9
|
+
}
|
|
10
|
+
pass('守门:必填与绝对日期(不猜)', ()=>{
|
|
11
|
+
const ev = {
|
|
12
|
+
destination: '大理',
|
|
13
|
+
start: '2025-10-01',
|
|
14
|
+
source: 'user-verbatim',
|
|
15
|
+
evidence: '用户原话'
|
|
16
|
+
};
|
|
17
|
+
assert.equal(appendTrip([], {
|
|
18
|
+
...ev,
|
|
19
|
+
destination: ''
|
|
20
|
+
}).appended, false, '无目的地拒');
|
|
21
|
+
assert.equal(appendTrip([], {
|
|
22
|
+
...ev,
|
|
23
|
+
evidence: ''
|
|
24
|
+
}).appended, false, '无 evidence 拒(溯源 P0)');
|
|
25
|
+
assert.ok(appendTrip([], {
|
|
26
|
+
...ev,
|
|
27
|
+
start: '下周一'
|
|
28
|
+
}).reason?.includes('YYYY-MM-DD'), '词表外日期拒收(不猜)');
|
|
29
|
+
assert.ok(appendTrip([], {
|
|
30
|
+
...ev,
|
|
31
|
+
end: '2025-09-30'
|
|
32
|
+
}).reason?.includes('end 早于 start'), 'end<start 拒');
|
|
33
|
+
});
|
|
34
|
+
pass('幂等:同目的地+start+source = 同一行程 no-op', ()=>{
|
|
35
|
+
const ev = {
|
|
36
|
+
destination: '大理',
|
|
37
|
+
start: '2025-10-01',
|
|
38
|
+
source: 'user-verbatim',
|
|
39
|
+
evidence: '原话'
|
|
40
|
+
};
|
|
41
|
+
const r1 = appendTrip([], ev);
|
|
42
|
+
assert.equal(r1.appended, true);
|
|
43
|
+
const r2 = appendTrip(r1.events, {
|
|
44
|
+
...ev,
|
|
45
|
+
evidence: '换个说法再说一遍'
|
|
46
|
+
});
|
|
47
|
+
assert.equal(r2.appended, false, '同 trip_id 重放 no-op');
|
|
48
|
+
assert.equal(r2.tripId, r1.tripId);
|
|
49
|
+
});
|
|
50
|
+
pass('重叠冲突即停:同目的地日期区间重叠拒收', ()=>{
|
|
51
|
+
const r1 = appendTrip([], {
|
|
52
|
+
destination: '大理',
|
|
53
|
+
start: '2025-10-01',
|
|
54
|
+
end: '2025-10-05',
|
|
55
|
+
source: 'user-verbatim',
|
|
56
|
+
evidence: '原话'
|
|
57
|
+
});
|
|
58
|
+
const r2 = appendTrip(r1.events, {
|
|
59
|
+
destination: '大理',
|
|
60
|
+
start: '2025-10-04',
|
|
61
|
+
source: 'user-verbatim',
|
|
62
|
+
evidence: '原话2'
|
|
63
|
+
});
|
|
64
|
+
assert.equal(r2.appended, false);
|
|
65
|
+
assert.ok(r2.reason?.includes('重叠'), '冲突理由说明');
|
|
66
|
+
});
|
|
67
|
+
pass('日期解析:锚点卡词表进、词表外 null(经 resolveTimelineDate)', ()=>{
|
|
68
|
+
assert.equal(resolveTimelineDate('2025-10-01', anchor), '2025-10-01');
|
|
69
|
+
assert.equal(resolveTimelineDate('去年国庆', anchor), null, '开放表达不猜');
|
|
70
|
+
});
|
|
71
|
+
pass('投影:start 倒序(最近在前)', ()=>{
|
|
72
|
+
let evs = [];
|
|
73
|
+
evs = appendTrip(evs, {
|
|
74
|
+
destination: '普吉',
|
|
75
|
+
start: '2026-07-10',
|
|
76
|
+
source: 'user-verbatim',
|
|
77
|
+
evidence: 'a'
|
|
78
|
+
}).events;
|
|
79
|
+
evs = appendTrip(evs, {
|
|
80
|
+
destination: '大理',
|
|
81
|
+
start: '2025-10-01',
|
|
82
|
+
source: 'user-verbatim',
|
|
83
|
+
evidence: 'b'
|
|
84
|
+
}).events;
|
|
85
|
+
const p = projectTimeline(evs);
|
|
86
|
+
assert.equal(p[0]?.destination, '普吉');
|
|
87
|
+
assert.equal(p.length, 2);
|
|
88
|
+
});
|
|
89
|
+
pass('交叉一致:verified wish 无 timeline = 缺口暴露(巡检口径,不自动补写)', ()=>{
|
|
90
|
+
const evs = appendTrip([], {
|
|
91
|
+
destination: '大理',
|
|
92
|
+
start: '2025-10-01',
|
|
93
|
+
source: 'wish-confirmed',
|
|
94
|
+
evidence: 'wish w1 confirm'
|
|
95
|
+
}).events;
|
|
96
|
+
const gaps = timelineGapsForVerified(evs, [
|
|
97
|
+
{
|
|
98
|
+
wishId: 'w1',
|
|
99
|
+
destination: '大理'
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
wishId: 'w2',
|
|
103
|
+
destination: '清迈'
|
|
104
|
+
}
|
|
105
|
+
]);
|
|
106
|
+
assert.deepEqual(gaps, [
|
|
107
|
+
{
|
|
108
|
+
wishId: 'w2'
|
|
109
|
+
}
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
pass('trip_id 语义派生:确定性可重放', ()=>{
|
|
113
|
+
assert.equal(makeTripId({
|
|
114
|
+
destination: '大 理',
|
|
115
|
+
start: '2025-10-01',
|
|
116
|
+
source: 'user-verbatim',
|
|
117
|
+
evidence: 'x'
|
|
118
|
+
}), '大_理|2025-10-01|user-verbatim');
|
|
119
|
+
});
|
|
120
|
+
console.log(`\nTRAVEL TIMELINE TESTS: ${n}/7 OK(memory-design P1 守门面)`);
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/travel-timeline-tests.ts
|
|
@@ -16,7 +16,7 @@ const byLeg = Object.fromEntries(r1.legs.map((l)=>[
|
|
|
16
16
|
]));
|
|
17
17
|
assert.notEqual(byLeg['f4'].service, 'DZ6252');
|
|
18
18
|
assert.equal(byLeg['f5'].service, 'EK329');
|
|
19
|
-
assert.equal(byLeg['f5'].energy_pct,
|
|
19
|
+
assert.equal(byLeg['f5'].energy_pct, 79);
|
|
20
20
|
assert.ok(byLeg['f5'].wake.includes('前一日'));
|
|
21
21
|
assert.equal(byLeg['f5'].door_to_door, '11h20m');
|
|
22
22
|
if (byLeg['f3'].service === 'MU6088') {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const SENSITIVE_PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
re: /\d{15,17}[Xx]?/,
|
|
4
|
+
label: '疑似证件号/卡号'
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
re: /1[3-9]\d{9}/,
|
|
8
|
+
label: '疑似手机号'
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
re: /(护照|身份证|证件号|通行证)\s*号?\s*[::]?\s*\w+/i,
|
|
12
|
+
label: '证件信息'
|
|
13
|
+
}
|
|
14
|
+
];
|
|
15
|
+
export function sensitiveViolation(text) {
|
|
16
|
+
for (const { re, label } of SENSITIVE_PATTERNS){
|
|
17
|
+
if (re.test(text)) return `负面清单拒收:${label}不入库(需要时由后端从登录态填充)`;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export function companionId(label) {
|
|
22
|
+
return label.replace(/\s+/g, '');
|
|
23
|
+
}
|
|
24
|
+
function mergeStrArray(oldArr, newArr) {
|
|
25
|
+
const a = oldArr ?? [];
|
|
26
|
+
let added = false;
|
|
27
|
+
for (const x of newArr ?? []){
|
|
28
|
+
if (!a.includes(x)) {
|
|
29
|
+
a.push(x);
|
|
30
|
+
added = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
merged: a,
|
|
35
|
+
added
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function upsertCompanion(profiles, patch) {
|
|
39
|
+
if (!patch.label?.trim()) return {
|
|
40
|
+
profiles,
|
|
41
|
+
appended: false,
|
|
42
|
+
companionId: '',
|
|
43
|
+
reason: 'label 必填'
|
|
44
|
+
};
|
|
45
|
+
const violation = sensitiveViolation(JSON.stringify(patch));
|
|
46
|
+
if (violation) return {
|
|
47
|
+
profiles,
|
|
48
|
+
appended: false,
|
|
49
|
+
companionId: '',
|
|
50
|
+
reason: violation
|
|
51
|
+
};
|
|
52
|
+
const id = companionId(patch.label);
|
|
53
|
+
const existing = profiles.find((p)=>p.companion_id === id);
|
|
54
|
+
if (!existing) {
|
|
55
|
+
const full = {
|
|
56
|
+
schema: 'companion_profile.v1',
|
|
57
|
+
companion_id: id,
|
|
58
|
+
label: patch.label.trim(),
|
|
59
|
+
constraints: {
|
|
60
|
+
mobility: patch.constraints.mobility,
|
|
61
|
+
health: patch.constraints.health ? [
|
|
62
|
+
...patch.constraints.health
|
|
63
|
+
] : undefined,
|
|
64
|
+
prefs: patch.constraints.prefs ? [
|
|
65
|
+
...patch.constraints.prefs
|
|
66
|
+
] : undefined
|
|
67
|
+
},
|
|
68
|
+
evidence: [
|
|
69
|
+
patch.evidence
|
|
70
|
+
],
|
|
71
|
+
ts: new Date().toISOString()
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
profiles: [
|
|
75
|
+
...profiles,
|
|
76
|
+
full
|
|
77
|
+
],
|
|
78
|
+
appended: true,
|
|
79
|
+
companionId: id
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const mergedConstraints = {
|
|
83
|
+
mobility: patch.constraints.mobility ?? existing.constraints.mobility,
|
|
84
|
+
health: mergeStrArray(existing.constraints.health, patch.constraints.health).merged,
|
|
85
|
+
prefs: mergeStrArray(existing.constraints.prefs, patch.constraints.prefs).merged
|
|
86
|
+
};
|
|
87
|
+
const ev = mergeStrArray(existing.evidence, [
|
|
88
|
+
patch.evidence
|
|
89
|
+
]);
|
|
90
|
+
const healthLen = mergedConstraints.health?.length ?? 0;
|
|
91
|
+
const prefsLen = mergedConstraints.prefs?.length ?? 0;
|
|
92
|
+
const changed = mergedConstraints.mobility !== existing.constraints.mobility || healthLen !== (existing.constraints.health?.length ?? 0) || prefsLen !== (existing.constraints.prefs?.length ?? 0) || ev.added;
|
|
93
|
+
if (!changed) return {
|
|
94
|
+
profiles,
|
|
95
|
+
appended: false,
|
|
96
|
+
companionId: id
|
|
97
|
+
};
|
|
98
|
+
const next = profiles.map((p)=>p.companion_id === id ? {
|
|
99
|
+
...p,
|
|
100
|
+
constraints: mergedConstraints,
|
|
101
|
+
evidence: ev.merged,
|
|
102
|
+
ts: new Date().toISOString()
|
|
103
|
+
} : p);
|
|
104
|
+
return {
|
|
105
|
+
profiles: next,
|
|
106
|
+
appended: true,
|
|
107
|
+
companionId: id
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/companions.ts
|