@xcanwin/manyoyo 8.6.2 → 8.6.3

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,182 @@
1
+ 'use strict';
2
+
3
+ // /messages 的分页、增量与 trace 精简(纯函数,不碰磁盘)。
4
+ // 稳定前缀约定:第一条 pending 消息之前的消息不再被修改;违反约定的写操作必须给该 agent 换新的 epoch。
5
+
6
+ const crypto = require('crypto');
7
+
8
+ // 响应里的 firstId:page 模式是本页第一条的 id;tail 模式是整份历史当前保留的第一条(前端据此裁掉服务端已丢弃的开头)
9
+ const MESSAGES_DEFAULT_LIMIT = 60;
10
+ const MESSAGES_MAX_LIMIT = 200;
11
+ // 页起点最多再往前多取这么多条,直到遇到 user 消息
12
+ const MESSAGES_ALIGN_LOOKBACK = 20;
13
+ // tail 一次最多返回这么多条,更多就让前端整页重置
14
+ const MESSAGES_TAIL_MAX = 120;
15
+ const TRACE_STRING_MAX = 512;
16
+ const TRACE_STRING_KEEP = 200;
17
+
18
+ const MESSAGE_ID_PATTERN = /^[0-9]+-[0-9a-f]{1,8}$/;
19
+ const EPOCH_PATTERN = /^[0-9a-f]{8,32}$/;
20
+ const CLIENT_MESSAGE_ID_PATTERN = /^[A-Za-z0-9_-]{8,64}$/;
21
+
22
+ function createEpoch() {
23
+ return crypto.randomBytes(6).toString('hex');
24
+ }
25
+
26
+ /**
27
+ * 解析并校验 /messages 的查询参数。
28
+ * @param {URLSearchParams} params
29
+ * @returns {{ error?: string, limit?: number, after?: string, before?: string, epoch?: string }}
30
+ */
31
+ function parseMessagesQuery(params) {
32
+ const result = { limit: MESSAGES_DEFAULT_LIMIT };
33
+ if (params.has('limit')) {
34
+ const raw = params.get('limit');
35
+ if (!/^[0-9]{1,3}$/.test(raw)) {
36
+ return { error: 'limit 必须是 1 到 200 的整数' };
37
+ }
38
+ const limit = Number(raw);
39
+ if (limit < 1 || limit > MESSAGES_MAX_LIMIT) {
40
+ return { error: 'limit 必须是 1 到 200 的整数' };
41
+ }
42
+ result.limit = limit;
43
+ }
44
+ if (params.has('after')) {
45
+ const after = params.get('after');
46
+ if (!MESSAGE_ID_PATTERN.test(after)) {
47
+ return { error: 'after 不是合法的消息 id' };
48
+ }
49
+ result.after = after;
50
+ }
51
+ if (params.has('before')) {
52
+ const before = params.get('before');
53
+ if (!MESSAGE_ID_PATTERN.test(before)) {
54
+ return { error: 'before 不是合法的消息 id' };
55
+ }
56
+ result.before = before;
57
+ }
58
+ if (params.has('epoch')) {
59
+ const epoch = params.get('epoch');
60
+ if (!EPOCH_PATTERN.test(epoch)) {
61
+ return { error: 'epoch 格式不正确' };
62
+ }
63
+ result.epoch = epoch;
64
+ }
65
+ if (result.after && result.before) {
66
+ return { error: 'after 与 before 不能同时使用' };
67
+ }
68
+ if (result.after && !result.epoch) {
69
+ return { error: 'after 需要同时提供 epoch' };
70
+ }
71
+ return result;
72
+ }
73
+
74
+ function truncateString(value) {
75
+ return value.length > TRACE_STRING_MAX ? `${value.slice(0, TRACE_STRING_KEEP)}…` : value;
76
+ }
77
+
78
+ // 递归截断 trace 事件里的长字符串,保留结构(kind、标题、状态、id 等)
79
+ function truncateTraceValue(value, state, depth) {
80
+ if (typeof value === 'string') {
81
+ const next = truncateString(value);
82
+ if (next !== value) {
83
+ state.truncated = true;
84
+ }
85
+ return next;
86
+ }
87
+ if (depth > 6 || !value || typeof value !== 'object') {
88
+ return value;
89
+ }
90
+ if (Array.isArray(value)) {
91
+ return value.map(item => truncateTraceValue(item, state, depth + 1));
92
+ }
93
+ const out = {};
94
+ Object.keys(value).forEach(key => {
95
+ out[key] = truncateTraceValue(value[key], state, depth + 1);
96
+ });
97
+ return out;
98
+ }
99
+
100
+ /** 列表里的 trace 消息精简版;非 trace 消息或没有需要截断的内容原样返回 */
101
+ function toListMessage(message) {
102
+ if (!message || message.streamTrace !== true) {
103
+ return message;
104
+ }
105
+ const state = { truncated: false };
106
+ const content = typeof message.content === 'string' ? truncateString(message.content) : message.content;
107
+ if (content !== message.content) {
108
+ state.truncated = true;
109
+ }
110
+ const traceEvents = Array.isArray(message.traceEvents)
111
+ ? message.traceEvents.map(event => truncateTraceValue(event, state, 0))
112
+ : message.traceEvents;
113
+ if (!state.truncated) {
114
+ return message;
115
+ }
116
+ return { ...message, content, traceEvents, traceTruncated: true };
117
+ }
118
+
119
+ function alignPageStart(messages, start) {
120
+ if (start <= 0 || (messages[start] && messages[start].role === 'user')) {
121
+ return Math.max(0, start);
122
+ }
123
+ const floor = Math.max(0, start - MESSAGES_ALIGN_LOOKBACK);
124
+ for (let i = start - 1; i >= floor; i--) {
125
+ if (messages[i] && messages[i].role === 'user') {
126
+ return i;
127
+ }
128
+ }
129
+ return start;
130
+ }
131
+
132
+ function buildPage(messages, end, limit) {
133
+ const start = alignPageStart(messages, Math.max(0, end - limit));
134
+ const page = messages.slice(start, end);
135
+ return {
136
+ messages: page.map(toListMessage),
137
+ // 页的第一条 id,同时是继续往前翻(before)的游标
138
+ firstId: page.length ? String(page[0].id || '') : '',
139
+ hasMore: start > 0
140
+ };
141
+ }
142
+
143
+ /**
144
+ * @param {Array} messages 某个 agent 的完整消息
145
+ * @param {string} epoch
146
+ * @param {{ limit: number, after?: string, before?: string, epoch?: string }} query
147
+ */
148
+ function buildMessagesResponse(messages, epoch, query) {
149
+ const list = Array.isArray(messages) ? messages : [];
150
+ if (query.after) {
151
+ const anchorIndex = query.epoch === epoch
152
+ ? list.findIndex(item => item && item.id === query.after)
153
+ : -1;
154
+ if (anchorIndex !== -1 && list.length - anchorIndex - 1 <= MESSAGES_TAIL_MAX) {
155
+ return {
156
+ messages: list.slice(anchorIndex + 1).map(toListMessage),
157
+ epoch,
158
+ firstId: list.length ? String(list[0].id || '') : '',
159
+ hasMore: false,
160
+ mode: 'tail'
161
+ };
162
+ }
163
+ return { ...buildPage(list, list.length, query.limit), epoch, mode: 'reset' };
164
+ }
165
+ if (query.before) {
166
+ const index = list.findIndex(item => item && item.id === query.before);
167
+ if (index === -1) {
168
+ return { messages: [], epoch, firstId: '', hasMore: false, mode: 'page' };
169
+ }
170
+ return { ...buildPage(list, index, query.limit), epoch, mode: 'page' };
171
+ }
172
+ return { ...buildPage(list, list.length, query.limit), epoch, mode: 'page' };
173
+ }
174
+
175
+ module.exports = {
176
+ MESSAGE_ID_PATTERN,
177
+ CLIENT_MESSAGE_ID_PATTERN,
178
+ createEpoch,
179
+ parseMessagesQuery,
180
+ toListMessage,
181
+ buildMessagesResponse
182
+ };