@xcanwin/manyoyo 8.6.11 → 8.7.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.
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ // 回退对话(Claude):web 历史里删掉那一轮及之后的消息之外,还要让 Claude 自己的会话也从那一轮之前接着往下走。
4
+ // 做法是下一轮命令追加 `--resume-session-at <uuid>`(隐藏参数,2.1.304 实测可用):在同一个会话文件里从 uuid 那条消息之后另起分支。
5
+ // uuid 从容器里的会话 jsonl 里找:对应那条用户提示词记录,往前沿 parentUuid 走到最近的一条 assistant 记录;
6
+ // 走到头都没有 = 这是第一轮,下一轮不带 -r,开新会话。
7
+ const { isClaudeSessionId } = require('./session-meta');
8
+
9
+ const RECORDS_LIMIT = 20000;
10
+ const TEXT_LIMIT = 4000;
11
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
12
+
13
+ // 在容器里跑的 node 脚本(只读):每条记录只留 uuid / parentUuid / 类型 / 时间,用户提示词再带上文本
14
+ const RECORDS_SCRIPT = [
15
+ "const fs=require('fs');",
16
+ "const rl=require('readline').createInterface({input:fs.createReadStream(process.argv[1])});",
17
+ 'const out=[];',
18
+ "rl.on('line',l=>{let r;try{r=JSON.parse(l)}catch(e){return}",
19
+ "if(!r||typeof r.uuid!=='string'||!['user','assistant','attachment','system'].includes(r.type))return;",
20
+ "const o={u:r.uuid,p:r.parentUuid||null,t:r.type,ts:r.timestamp||''};",
21
+ "if(r.type==='user'&&!r.isSidechain){const c=r.message&&r.message.content;let x='';",
22
+ "if(typeof c==='string')x=c;",
23
+ "else if(Array.isArray(c)&&!c.some(b=>b&&b.type==='tool_result'))x=c.filter(b=>b&&b.type==='text').map(b=>b.text).join('\\n');",
24
+ `o.x=x.slice(0,${TEXT_LIMIT})}`,
25
+ 'out.push(o)});',
26
+ `rl.on('close',()=>process.stdout.write(JSON.stringify(out.slice(-${RECORDS_LIMIT}))));`
27
+ ].join('');
28
+
29
+ function quote(value) {
30
+ return `'${String(value).replace(/'/g, `'"'"'`)}'`;
31
+ }
32
+
33
+ function buildClaudeRecordsCommand(sessionId) {
34
+ if (!isClaudeSessionId(sessionId)) return '';
35
+ return [
36
+ `for f in "\${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/projects/*/${sessionId}.jsonl; do`,
37
+ `[ -f "$f" ] && timeout 20 node -e ${quote(RECORDS_SCRIPT)} "$f";`,
38
+ 'break; done'
39
+ ].join(' ');
40
+ }
41
+
42
+ function parseClaudeRecords(output) {
43
+ try {
44
+ const list = JSON.parse(String(output || '').trim());
45
+ return Array.isArray(list) ? list.filter(item => item && typeof item.u === 'string') : [];
46
+ } catch (e) {
47
+ return [];
48
+ }
49
+ }
50
+
51
+ /**
52
+ * @returns {{found: boolean, uuid?: string|null}} uuid 为 null 表示回退到第一轮(该清掉引擎会话)
53
+ */
54
+ function pickClaudeResumeAt(records, { text, timeMs }) {
55
+ const needle = String(text || '').trim();
56
+ if (!needle) return { found: false };
57
+ const candidates = records.filter(item => item.t === 'user' && typeof item.x === 'string' && item.x.includes(needle));
58
+ if (!candidates.length) return { found: false };
59
+ const distance = item => {
60
+ const at = Date.parse(item.ts);
61
+ return Number.isFinite(at) && Number.isFinite(timeMs) ? Math.abs(at - timeMs) : Number.MAX_SAFE_INTEGER;
62
+ };
63
+ const target = candidates.reduce((best, item) => (distance(item) < distance(best) ? item : best));
64
+ const byId = new Map(records.map(item => [item.u, item]));
65
+ const seen = new Set();
66
+ let current = target.p ? byId.get(target.p) : null;
67
+ while (current && current.t !== 'assistant' && !seen.has(current.u)) {
68
+ seen.add(current.u);
69
+ current = current.p ? byId.get(current.p) : null;
70
+ }
71
+ if (!current || current.t !== 'assistant') return { found: true, uuid: null };
72
+ return UUID_PATTERN.test(current.u) ? { found: true, uuid: current.u } : { found: false };
73
+ }
74
+
75
+ module.exports = { UUID_PATTERN, buildClaudeRecordsCommand, parseClaudeRecords, pickClaudeResumeAt };