koishi-plugin-chatluna-studio 1.0.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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/dist/index.js +45313 -0
  4. package/dist/style.css +1 -0
  5. package/lib/chatluna/error.d.ts +15 -0
  6. package/lib/chatluna/runtime.d.ts +25 -0
  7. package/lib/chatluna/session-tracker.d.ts +98 -0
  8. package/lib/chatluna/usage.d.ts +46 -0
  9. package/lib/console-contract.d.ts +61 -0
  10. package/lib/console.d.ts +39 -0
  11. package/lib/evidence-kind.d.ts +61 -0
  12. package/lib/evidence-preview-text.d.ts +13 -0
  13. package/lib/index.d.ts +42 -0
  14. package/lib/index.js +4579 -0
  15. package/lib/model-evidence/index.d.ts +15 -0
  16. package/lib/model-evidence/metrics.d.ts +11 -0
  17. package/lib/model-evidence/request.d.ts +7 -0
  18. package/lib/model-evidence/response.d.ts +7 -0
  19. package/lib/model-evidence/shared.d.ts +37 -0
  20. package/lib/model-evidence/transport.d.ts +13 -0
  21. package/lib/model-evidence/types.d.ts +104 -0
  22. package/lib/model-evidence/usage.d.ts +10 -0
  23. package/lib/model-request-collector.d.ts +32 -0
  24. package/lib/model-request-trajectory.d.ts +21 -0
  25. package/lib/model-request-variables.d.ts +16 -0
  26. package/lib/model-request.d.ts +158 -0
  27. package/lib/persistence.d.ts +85 -0
  28. package/lib/presets/evidence-match.d.ts +9 -0
  29. package/lib/presets/index.d.ts +6 -0
  30. package/lib/presets/repository.d.ts +86 -0
  31. package/lib/presets/runtime-correlation.d.ts +16 -0
  32. package/lib/presets/service.d.ts +69 -0
  33. package/lib/presets/source-document.d.ts +2 -0
  34. package/lib/presets/types.d.ts +63 -0
  35. package/lib/record-store.d.ts +85 -0
  36. package/lib/types.d.ts +270 -0
  37. package/package.json +110 -0
package/lib/index.js ADDED
@@ -0,0 +1,4579 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name2 in all)
10
+ __defProp(target, name2, { get: all[name2], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ CHATLUNA_ERROR_CODE_DOCUMENTATION_URL: () => CHATLUNA_ERROR_CODE_DOCUMENTATION_URL,
34
+ ChatLunaSessionTracker: () => ChatLunaSessionTracker,
35
+ Config: () => Config,
36
+ DEFAULT_MODEL_REQUEST_PAGE_SIZE: () => DEFAULT_MODEL_REQUEST_PAGE_SIZE,
37
+ DEFAULT_MODEL_REQUEST_RECORD_LIMIT: () => DEFAULT_MODEL_REQUEST_RECORD_LIMIT,
38
+ DEFAULT_MODEL_REQUEST_RECORD_MAX_BYTES: () => DEFAULT_MODEL_REQUEST_RECORD_MAX_BYTES,
39
+ FileSystemPresetRepository: () => FileSystemPresetRepository,
40
+ InMemoryModelRequestRecords: () => InMemoryModelRequestRecords,
41
+ InMemoryRecordRows: () => InMemoryRecordRows,
42
+ KoishiDatabaseModelRequestPersistence: () => KoishiDatabaseModelRequestPersistence,
43
+ MAX_MODEL_REQUEST_PAGE_SIZE: () => MAX_MODEL_REQUEST_PAGE_SIZE,
44
+ MODEL_REQUEST_FACET_SCAN_LIMIT: () => MODEL_REQUEST_FACET_SCAN_LIMIT,
45
+ PresetRepositoryError: () => PresetRepositoryError,
46
+ PresetRuntimeSnapshotTracker: () => PresetRuntimeSnapshotTracker,
47
+ STUDIO_EVIDENCE_AGGREGATE_IDS: () => STUDIO_EVIDENCE_AGGREGATE_IDS,
48
+ STUDIO_EVIDENCE_KINDS: () => STUDIO_EVIDENCE_KINDS,
49
+ ScopeRowIndex: () => ScopeRowIndex,
50
+ SerialWriteQueue: () => SerialWriteQueue,
51
+ StudioDomainError: () => StudioDomainError,
52
+ StudioModelRequestCursorExpiredError: () => StudioModelRequestCursorExpiredError,
53
+ StudioModelRequestStore: () => StudioModelRequestStore,
54
+ StudioPresetService: () => StudioPresetService,
55
+ apply: () => apply,
56
+ archiveChatLunaModelRequestError: () => archiveChatLunaModelRequestError,
57
+ buildStudioModelRequestTrajectory: () => buildStudioModelRequestTrajectory,
58
+ buildStudioModelRequestTrajectoryFromStore: () => buildStudioModelRequestTrajectoryFromStore,
59
+ countMessageCharacters: () => countMessageCharacters,
60
+ countToolCallCharacters: () => countToolCallCharacters,
61
+ countToolDefinitionCharacters: () => countToolDefinitionCharacters,
62
+ createModelRequestError: () => createModelRequestError,
63
+ deriveModelRequestVariables: () => deriveModelRequestVariables,
64
+ estimateModelRequestRecordBytes: () => estimateModelRequestRecordBytes,
65
+ expressionStableId: () => expressionStableId,
66
+ findLatestFailedModelRequest: () => findLatestFailedModelRequest,
67
+ getChatLunaErrorPossibleCauses: () => getChatLunaErrorPossibleCauses,
68
+ inferModelResponseBodyFormat: () => inferModelResponseBodyFormat,
69
+ inject: () => inject,
70
+ installModelRequestCollector: () => installModelRequestCollector,
71
+ isKnownChatModelRequestUrl: () => isKnownChatModelRequestUrl,
72
+ isStudioEvidenceAggregateMember: () => isStudioEvidenceAggregateMember,
73
+ isVirtualOneBotSession: () => isVirtualOneBotSession,
74
+ linkChatLunaUsageRequest: () => linkChatLunaUsageRequest,
75
+ lookupChatLunaUsage: () => lookupChatLunaUsage,
76
+ matchPresetExpressionEvidence: () => matchPresetExpressionEvidence,
77
+ matchesModelRequestQuery: () => matchesModelRequestQuery,
78
+ modelRequestVariableStatusLabel: () => modelRequestVariableStatusLabel,
79
+ name: () => name,
80
+ parsePresetSourceDocument: () => parsePresetSourceDocument,
81
+ presentModelRequestDetail: () => presentModelRequestDetail,
82
+ presentModelRequestListItem: () => presentModelRequestListItem,
83
+ projectModelEvidence: () => projectModelEvidence,
84
+ readChatLunaRequestError: () => readChatLunaRequestError,
85
+ readStudioModelRequestConversationRecords: () => readStudioModelRequestConversationRecords,
86
+ readStudioSessionEntities: () => readStudioSessionEntities,
87
+ registerConsole: () => registerConsole,
88
+ registerStudioModelRequestModel: () => registerStudioModelRequestModel,
89
+ resolveChatLunaPluginClass: () => resolveChatLunaPluginClass,
90
+ resolveConsoleEntry: () => resolveConsoleEntry,
91
+ resolveModelRequestOrder: () => resolveModelRequestOrder,
92
+ sanitizeModelRequestUrl: () => sanitizeModelRequestUrl,
93
+ statusLabel: () => statusLabel,
94
+ studioEvidenceAggregate: () => studioEvidenceAggregate,
95
+ studioEvidenceLabels: () => studioEvidenceLabels,
96
+ toModelRequestRecordHeader: () => toModelRequestRecordHeader,
97
+ toStudioModelRequestUsage: () => toStudioModelRequestUsage,
98
+ usage: () => usage
99
+ });
100
+ module.exports = __toCommonJS(index_exports);
101
+ var import_koishi3 = require("koishi");
102
+
103
+ // src/chatluna/error.ts
104
+ var CHATLUNA_ERROR_CODE_DOCUMENTATION_URL = "https://chatluna.chat/guide/faq/error_code.html";
105
+ var CHATLUNA_ERROR_CAUSES = {
106
+ 1: ["\u7F51\u7EDC\u8FDE\u63A5\u6216\u4EE3\u7406\u914D\u7F6E\u5F02\u5E38\u3002"],
107
+ 2: ["\u4EE3\u7406\u5730\u5740\u6216\u4EE3\u7406\u534F\u8BAE\u65E0\u6548\uFF0C\u8BF7\u68C0\u67E5\u4EE3\u7406 URL \u7684\u534F\u8BAE\u548C\u683C\u5F0F\u3002"],
108
+ 100: ["API Key \u4E0D\u53EF\u7528\u6216\u65E0\u6548\uFF0C\u8BF7\u786E\u8BA4\u5BC6\u94A5\u4ECD\u53EF\u6B63\u5E38\u4F7F\u7528\u3002"],
109
+ 101: ["\u670D\u52A1\u5546\u8981\u6C42\u5B8C\u6210\u9A8C\u8BC1\u7801\uFF0C\u8BF7\u767B\u5F55\u5BF9\u5E94\u670D\u52A1\u624B\u52A8\u9A8C\u8BC1\u3002"],
110
+ 102: ["\u7F51\u7EDC\u8FDE\u63A5\u3001\u4EE3\u7406\u914D\u7F6E\u6216 API Key \u5F02\u5E38\u3002", "\u6A21\u578B\u54CD\u5E94\u8F83\u6162\u3001\u4E0A\u4E0B\u6587\u8F83\u957F\u6216\u4EE3\u7406\u5EF6\u8FDF\u8F83\u9AD8\uFF1B\u786E\u8BA4\u8FD9\u4E9B\u60C5\u51B5\u540E\u53EF\u9002\u5F53\u589E\u52A0\u8D85\u65F6\u65F6\u95F4\u3002"],
111
+ 103: ["\u7F51\u7EDC\u8FDE\u63A5\u3001\u4EE3\u7406\u914D\u7F6E\u6216 API Key \u5F02\u5E38\uFF1B\u8BE5\u9519\u8BEF\u7801\u8986\u76D6\u8303\u56F4\u8F83\u5E7F\uFF0C\u8BF7\u7ED3\u5408\u4E0B\u65B9\u539F\u59CB\u539F\u56E0\u548C\u54CD\u5E94\u539F\u6587\u7EE7\u7EED\u5224\u65AD\u3002"],
112
+ 104: ["\u8BF7\u6C42\u6216\u5BF9\u8BDD\u4E2D\u5305\u542B\u4E0D\u5B89\u5168\u5185\u5BB9\uFF0C\u8BF7\u65B0\u5EFA\u4F1A\u8BDD\u540E\u91CD\u8BD5\u3002"],
113
+ 301: ["\u6A21\u578B\u4E0D\u53EF\u7528\u3001\u6A21\u578B\u540D\u79F0\u4E0D\u5B58\u5728\uFF0C\u6216\u9002\u914D\u5668\u521D\u59CB\u5316\u5931\u8D25\u3002"],
114
+ 303: ["\u6A21\u578B\u521D\u59CB\u5316\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6A21\u578B\u914D\u7F6E\u548C\u7F51\u7EDC\u8FDE\u63A5\u3002"],
115
+ 307: ["\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u914D\u7F6E\uFF1B\u53EF\u5148\u65B0\u5EFA\u4F1A\u8BDD\uFF0C\u518D\u68C0\u67E5\u6A21\u578B\u3001\u5D4C\u5165\u6A21\u578B\u548C\u5411\u91CF\u6570\u636E\u5E93\u914D\u7F6E\u3002"],
116
+ 309: ["\u6A21\u578B\u8FD4\u56DE\u7A7A\u54CD\u5E94\uFF1B\u8BF7\u68C0\u67E5 API\u3001\u7F51\u7EDC\u548C\u5BC6\u94A5\uFF0C\u5E76\u786E\u8BA4\u5BF9\u8BDD\u6216\u9884\u8BBE\u5185\u5BB9\u672A\u88AB\u6A21\u578B\u62D2\u7EDD\u3002"]
117
+ };
118
+ function readChatLunaRequestError(error) {
119
+ const value = readRecord(error);
120
+ if (!value) return;
121
+ const code = readFiniteNumber(value.errorCode);
122
+ const message = readNonEmptyString(value.message);
123
+ const originMessage = readOriginMessage(value.originError);
124
+ const isTimeout = value.isTimeout === true;
125
+ if (code === void 0 && !message && !originMessage && !isTimeout) return;
126
+ return {
127
+ ...code !== void 0 ? { code } : {},
128
+ ...message ? { message } : {},
129
+ ...originMessage && originMessage !== message ? { originMessage } : {},
130
+ ...isTimeout ? { isTimeout: true } : {}
131
+ };
132
+ }
133
+ function getChatLunaErrorPossibleCauses(error) {
134
+ if (error?.code === void 0) return [];
135
+ return CHATLUNA_ERROR_CAUSES[error.code] ?? [];
136
+ }
137
+ function findLatestFailedModelRequest(records, conversationId) {
138
+ const candidates = records.filter((record) => record.status === "error" && !record.chatlunaError).sort((left, right) => right.sequence - left.sequence);
139
+ if (conversationId) {
140
+ const matchingConversation = candidates.find((record) => record.entities.conversationId === conversationId);
141
+ if (matchingConversation) return matchingConversation;
142
+ }
143
+ return candidates[0];
144
+ }
145
+ function archiveChatLunaModelRequestError(store, error, target) {
146
+ const chatlunaError = readChatLunaRequestError(error);
147
+ if (!chatlunaError) return;
148
+ const task = store.getRawRecords().then(async (records) => {
149
+ const record = findLatestFailedModelRequest(records, target.conversationId);
150
+ if (!record) return;
151
+ await store.update(record.id, { chatlunaError });
152
+ }).catch(() => void 0);
153
+ store.trackUpdate(task);
154
+ }
155
+ function readRecord(value) {
156
+ return value !== null && typeof value === "object" ? value : void 0;
157
+ }
158
+ function readNonEmptyString(value) {
159
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
160
+ }
161
+ function readFiniteNumber(value) {
162
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
163
+ }
164
+ function readOriginMessage(origin) {
165
+ const record = readRecord(origin);
166
+ return readNonEmptyString(record?.message) ?? readNonEmptyString(origin);
167
+ }
168
+
169
+ // src/chatluna/session-tracker.ts
170
+ var import_koishi = require("koishi");
171
+ function readRecord2(value) {
172
+ return value !== null && typeof value === "object" ? value : void 0;
173
+ }
174
+ function readString(value) {
175
+ return typeof value === "string" && value ? value : void 0;
176
+ }
177
+ function readNested(value, path) {
178
+ let current = value;
179
+ for (const key of path) current = readRecord2(current)?.[key];
180
+ return current;
181
+ }
182
+ function readFirstString(value, paths) {
183
+ for (const path of paths) {
184
+ const found = readString(readNested(value, path));
185
+ if (found) return found;
186
+ }
187
+ }
188
+ function resolveConversationType(session, conversationId) {
189
+ if (readNested(session, ["isDirect"]) === true) return "private";
190
+ if (readString(readNested(session, ["subtype"])) === "private") return "private";
191
+ if (conversationId.startsWith("private:")) return "private";
192
+ return readFirstString(session, [["guildId"], ["event", "guild", "id"]]) ? "group" : "private";
193
+ }
194
+ function readStudioSessionEntities(session) {
195
+ const botId = readFirstString(session, [["selfId"], ["bot", "selfId"], ["bot", "userId"]]);
196
+ const conversationId = readFirstString(session, [["channelId"], ["channel", "id"], ["event", "channel", "id"]]);
197
+ if (!botId || !conversationId) return;
198
+ const platform = readFirstString(session, [["platform"], ["bot", "platform"]]);
199
+ const botName = readFirstString(session, [["bot", "user", "name"], ["bot", "user", "nick"], ["bot", "nickname"]]);
200
+ const guildId = readFirstString(session, [["guildId"], ["event", "guild", "id"]]);
201
+ const userId = readFirstString(session, [["userId"], ["author", "id"], ["event", "user", "id"]]);
202
+ const conversationName = readFirstString(session, [
203
+ ["channel", "name"],
204
+ ["event", "channel", "name"],
205
+ ["guild", "name"],
206
+ ["event", "guild", "name"]
207
+ ]);
208
+ const userName = readFirstString(session, [
209
+ ["author", "nick"],
210
+ ["author", "nickname"],
211
+ ["author", "name"],
212
+ ["username"],
213
+ ["event", "user", "name"]
214
+ ]);
215
+ return {
216
+ ...platform ? { platform } : {},
217
+ botId,
218
+ ...botName ? { botName } : {},
219
+ conversationId,
220
+ ...conversationName ? { conversationName } : {},
221
+ conversationType: resolveConversationType(session, conversationId),
222
+ ...guildId ? { guildId } : {},
223
+ ...userId ? { userId } : {},
224
+ ...userName ? { userName } : {}
225
+ };
226
+ }
227
+ function isVirtualOneBotSession(session) {
228
+ if (readNested(session, ["bot", "hidden"]) !== true) return false;
229
+ return readFirstString(session, [["bot", "platform"], ["platform"]]) === "onebot";
230
+ }
231
+ var ChatLunaSessionTracker = class {
232
+ constructor(ctx, options = {}) {
233
+ this.options = options;
234
+ const on = ctx.on.bind(ctx);
235
+ this.disposers.push(on("chatluna/before-chat", (conversationId, _message, _variables, _chatInterface, session) => {
236
+ const key = this.begin(session);
237
+ if (!key) return;
238
+ const keys = this.coreKeys.get(conversationId) ?? /* @__PURE__ */ new Set();
239
+ keys.add(key);
240
+ this.coreKeys.set(conversationId, keys);
241
+ }));
242
+ this.disposers.push(on("chatluna/after-chat", (conversationId, _source, _response, _variables, _chatInterface, session) => {
243
+ this.finishCore(conversationId, session);
244
+ }));
245
+ this.disposers.push(on("chatluna/after-chat-error", (error, conversationId) => {
246
+ const turn = this.resolveCoreTurn(conversationId);
247
+ if (turn && !turn.virtual) this.options.onTurnFailed?.(error, turn);
248
+ this.finishCore(conversationId);
249
+ }));
250
+ this.disposers.push(on("chatluna_character/before-chat", (payload) => {
251
+ this.begin(readRecord2(payload)?.session);
252
+ }));
253
+ this.disposers.push(on("chatluna_character/after-chat", (payload) => {
254
+ this.finish(readRecord2(payload)?.session);
255
+ }));
256
+ }
257
+ options;
258
+ /** 正在进行的对话轮,按(机器人,会话)索引;同一会话重复开始时以后一次为准。 */
259
+ turns = /* @__PURE__ */ new Map();
260
+ /**
261
+ * ChatLuna 内部会话标识到本地键的映射。
262
+ *
263
+ * 核心链路的结束事件只带内部会话标识,不带 session:出错时 `after-chat-error` 连 session
264
+ * 都没有,只有这张映射能把它还原成具体的(机器人,会话)。
265
+ */
266
+ coreKeys = /* @__PURE__ */ new Map();
267
+ disposers = [];
268
+ /** 内部会话标识唯一对应一轮时才认;映射到多轮时这次错误归属不明。 */
269
+ resolveCoreTurn(conversationId) {
270
+ const keys = this.coreKeys.get(conversationId);
271
+ if (keys?.size !== 1) return;
272
+ const [key] = keys;
273
+ return key ? this.turns.get(key) : void 0;
274
+ }
275
+ /**
276
+ * 归属候选,外加「这次请求是否确定来自虚拟机器人」。
277
+ *
278
+ * 归属恰好一轮时才交出:多轮并发或没有任何一轮在跑时,这次模型请求归属不明。
279
+ *
280
+ * `virtualOnly` 只在「在跑的对话轮非空且全部虚拟」时为真。虚拟轮与真实轮并发时它为假:fetch
281
+ * 边界分不出这次请求是哪一边发的,把整对都丢掉会连真实会话的证据一起丢,只能记成未归属留给
282
+ * 人工按时间对照。反过来两轮都虚拟时不能只看「恰好一轮」,否则模拟环境里跑多轮就又漏进来了。
283
+ *
284
+ * 判定留在这里而不是采集器里:采集器只知道「拿到候选就写归属」,而「几轮算可判定」「哪种机器人
285
+ * 不该记」都是会话跟踪自己的规则,两个模块因此不必共享一份并发假设。
286
+ */
287
+ resolveActiveTurn() {
288
+ const active = [...this.turns.values()];
289
+ const virtualOnly = active.length > 0 && active.every(({ virtual }) => virtual);
290
+ if (active.length !== 1) return { virtualOnly };
291
+ const [turn] = active;
292
+ if (!turn || turn.virtual) return { virtualOnly };
293
+ return { virtualOnly, turn: { entities: { ...turn.entities }, interactionId: turn.interactionId } };
294
+ }
295
+ /** 当前正在进行的对话轮数;诊断与测试用。 */
296
+ get activeTurnCount() {
297
+ return this.turns.size;
298
+ }
299
+ dispose() {
300
+ for (const dispose of this.disposers.splice(0)) dispose();
301
+ this.turns.clear();
302
+ this.coreKeys.clear();
303
+ }
304
+ begin(session) {
305
+ const entities = readStudioSessionEntities(session);
306
+ if (!entities) return;
307
+ const key = turnKey(entities);
308
+ const existing = this.turns.get(key);
309
+ this.turns.set(key, {
310
+ entities,
311
+ interactionId: existing?.interactionId ?? import_koishi.Random.id(),
312
+ virtual: isVirtualOneBotSession(session)
313
+ });
314
+ return key;
315
+ }
316
+ finish(session) {
317
+ const entities = readStudioSessionEntities(session);
318
+ if (entities) this.turns.delete(turnKey(entities));
319
+ }
320
+ finishCore(conversationId, session) {
321
+ const entities = readStudioSessionEntities(session);
322
+ if (entities) {
323
+ const key = turnKey(entities);
324
+ this.turns.delete(key);
325
+ const keys2 = this.coreKeys.get(conversationId);
326
+ keys2?.delete(key);
327
+ if (keys2 && !keys2.size) this.coreKeys.delete(conversationId);
328
+ return;
329
+ }
330
+ const keys = this.coreKeys.get(conversationId);
331
+ if (!keys) return;
332
+ if (keys.size === 1) for (const key of keys) this.turns.delete(key);
333
+ this.coreKeys.delete(conversationId);
334
+ }
335
+ };
336
+ function turnKey(entities) {
337
+ return `${entities.botId ?? ""}\0${entities.conversationId ?? ""}`;
338
+ }
339
+
340
+ // src/chatluna/usage.ts
341
+ var USAGE_MATCH_WINDOW_MS = 6e4;
342
+ function toStudioModelRequestUsage(row) {
343
+ return {
344
+ inputTokens: row.inputTokens,
345
+ outputTokens: row.outputTokens,
346
+ reasoningTokens: row.reasoningTokens,
347
+ cachedTokens: row.cachedTokens,
348
+ totalTokens: row.totalTokens,
349
+ ...row.ttftMs != null ? { ttftMs: row.ttftMs } : {},
350
+ ...row.totalMs != null ? { totalMs: row.totalMs } : {},
351
+ ...row.tps != null ? { tps: row.tps } : {},
352
+ ...row.estimated !== void 0 ? { estimated: row.estimated } : {},
353
+ source: "chatluna-usage"
354
+ };
355
+ }
356
+ async function linkChatLunaUsageRequest(stores, payload) {
357
+ const requestId = payload.context?.requestId?.trim();
358
+ if (!requestId) return;
359
+ const createdAt = payload.createdAt ? Date.parse(String(payload.createdAt)) : Date.now();
360
+ let best;
361
+ for (const store of stores) {
362
+ for (const record of await store.getRawRecords({ limit: 50 })) {
363
+ if (record.chatlunaRequestId) continue;
364
+ const delta = Math.abs(Date.parse(record.createdAt) - createdAt);
365
+ if (!Number.isFinite(delta) || delta > USAGE_MATCH_WINDOW_MS) continue;
366
+ const modelScore = modelsOverlap(record.model, payload.model) ? 0 : 1e4;
367
+ const score = delta + modelScore;
368
+ if (!best || score < best.score) best = { store, record, score };
369
+ }
370
+ }
371
+ if (!best) return;
372
+ best.store.update(best.record.id, { chatlunaRequestId: requestId });
373
+ return best.record.id;
374
+ }
375
+ async function lookupChatLunaUsage(service, record) {
376
+ if (!service) return;
377
+ const createdAt = Date.parse(record.createdAt);
378
+ if (!Number.isFinite(createdAt)) return;
379
+ const listed = await service.list({
380
+ start: new Date(createdAt - 3e4),
381
+ end: new Date(createdAt + Math.max(record.durationMs, 0) + 3e4),
382
+ pageSize: 50,
383
+ listSortBy: "createdAt",
384
+ listDesc: true
385
+ });
386
+ const rows = listed.rows ?? [];
387
+ const matched = record.chatlunaRequestId ? rows.find((row2) => row2.requestId === record.chatlunaRequestId) : void 0;
388
+ const row = matched ?? pickClosestUsageRow(rows, record);
389
+ return row ? toStudioModelRequestUsage(row) : void 0;
390
+ }
391
+ function pickClosestUsageRow(rows, record) {
392
+ const createdAt = Date.parse(record.createdAt);
393
+ let best;
394
+ for (const row of rows) {
395
+ const delta = Math.abs(Date.parse(String(row.createdAt)) - createdAt);
396
+ if (!Number.isFinite(delta) || delta > USAGE_MATCH_WINDOW_MS) continue;
397
+ const score = delta + (modelsOverlap(record.model, row.model) ? 0 : 1e4);
398
+ if (!best || score < best.score) best = { row, score };
399
+ }
400
+ return best?.row;
401
+ }
402
+ function modelsOverlap(left, right) {
403
+ if (!left || !right) return false;
404
+ return left === right || left.includes(right) || right.includes(left);
405
+ }
406
+
407
+ // src/console.ts
408
+ var import_node_fs = require("fs");
409
+ var import_node_path = require("path");
410
+
411
+ // src/model-request.ts
412
+ var import_koishi2 = require("koishi");
413
+
414
+ // src/model-evidence/shared.ts
415
+ function isRecord(value) {
416
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
417
+ }
418
+ function stringValue(value) {
419
+ return typeof value === "string" && value ? value : void 0;
420
+ }
421
+ function stringifyValue(value) {
422
+ if (typeof value === "string") return value;
423
+ try {
424
+ return JSON.stringify(value, null, 2) ?? String(value);
425
+ } catch {
426
+ return String(value);
427
+ }
428
+ }
429
+ function source(region, path, value) {
430
+ return { region, path, value };
431
+ }
432
+ function evidenceId(region, kind, path) {
433
+ return `${region === "request" ? "req" : "res"}:${kind}:${path.join(".")}`;
434
+ }
435
+ function semanticText(parts) {
436
+ return parts.filter((part) => part.kind === "text" || part.kind === "other").map((part) => part.value).join("\n");
437
+ }
438
+ function mediaSource(value) {
439
+ if (typeof value === "string") return value;
440
+ if (!isRecord(value)) return void 0;
441
+ if (typeof value.url === "string") return value.url;
442
+ if (typeof value.data === "string") {
443
+ const mimeType = stringValue(value.media_type) ?? stringValue(value.mimeType) ?? stringValue(value.mediaType);
444
+ return mimeType ? `data:${mimeType};base64,${value.data}` : value.data;
445
+ }
446
+ }
447
+ function normalizeContentParts(value) {
448
+ if (typeof value === "string") return [{ kind: "text", value }];
449
+ if (isRecord(value)) {
450
+ if (Array.isArray(value.parts)) return normalizeContentParts(value.parts);
451
+ return normalizeContentParts([value]);
452
+ }
453
+ if (!Array.isArray(value)) return value === void 0 ? [] : [{ kind: "other", value: stringifyValue(value) }];
454
+ return value.flatMap((item) => {
455
+ if (typeof item === "string") return [{ kind: "text", value: item }];
456
+ if (!isRecord(item)) return [];
457
+ if (isRecord(item.inlineData) || isRecord(item.fileData)) return normalizeGeminiPart(item);
458
+ const text = item.text ?? item.output_text ?? item.content;
459
+ if (typeof text === "string") return [{ kind: "text", value: text }];
460
+ const type = typeof item.type === "string" ? item.type.toLowerCase() : "";
461
+ const mimeType = stringValue(item.mediaType) ?? stringValue(item.mimeType) ?? stringValue(item.media_type);
462
+ const media = mediaSource(item.image_url ?? item.image ?? item.source ?? item.url ?? item.data);
463
+ if (type.includes("image") || mimeType?.startsWith("image/") || item.image_url || item.image) {
464
+ return [{ kind: "image", value: media ?? "[\u56FE\u7247]", ...mimeType ? { mimeType } : {} }];
465
+ }
466
+ if (type.includes("file") || mimeType && !mimeType.startsWith("image/")) {
467
+ return [{ kind: "file", value: media ?? "[\u6587\u4EF6]", ...mimeType ? { mimeType } : {} }];
468
+ }
469
+ if (type.includes("audio")) return [{ kind: "audio", value: media ?? "[\u97F3\u9891]", ...mimeType ? { mimeType } : {} }];
470
+ if (type.includes("video")) return [{ kind: "video", value: media ?? "[\u89C6\u9891]", ...mimeType ? { mimeType } : {} }];
471
+ return [{ kind: "other", value: stringifyValue(item) }];
472
+ });
473
+ }
474
+ function normalizeGeminiPart(value) {
475
+ if (!isRecord(value)) return [];
476
+ if (typeof value.text === "string") return [{ kind: "text", value: value.text }];
477
+ if (isRecord(value.inlineData)) {
478
+ const mimeType = stringValue(value.inlineData.mimeType);
479
+ const data = typeof value.inlineData.data === "string" ? value.inlineData.data : "";
480
+ if (mimeType?.startsWith("image/") && mimeType !== "image/svg+xml") {
481
+ return [{ kind: "image", value: `data:${mimeType};base64,${data}`, mimeType }];
482
+ }
483
+ return [{
484
+ kind: mimeType?.startsWith("audio/") ? "audio" : mimeType?.startsWith("video/") ? "video" : "file",
485
+ value: data || "[\u5185\u8054\u6587\u4EF6]",
486
+ ...mimeType ? { mimeType } : {}
487
+ }];
488
+ }
489
+ if (isRecord(value.fileData)) {
490
+ const mimeType = stringValue(value.fileData.mimeType);
491
+ const media = stringValue(value.fileData.fileUri) ?? "[\u6587\u4EF6]";
492
+ return [{
493
+ kind: mimeType?.startsWith("image/") ? "image" : mimeType?.startsWith("audio/") ? "audio" : mimeType?.startsWith("video/") ? "video" : "file",
494
+ value: media,
495
+ ...mimeType ? { mimeType } : {}
496
+ }];
497
+ }
498
+ return [{ kind: "other", value: stringifyValue(value) }];
499
+ }
500
+ function normalizeToolOutput(value) {
501
+ if (isRecord(value) && value.type === "json" && "value" in value) return stringifyValue(value.value);
502
+ if (isRecord(value) && value.type === "text" && typeof value.value === "string") return value.value;
503
+ return stringifyValue(value ?? "");
504
+ }
505
+ function readCallId(value) {
506
+ return stringValue(value.tool_call_id) ?? stringValue(value.toolCallId) ?? stringValue(value.call_id) ?? stringValue(value.callId) ?? stringValue(value.tool_use_id);
507
+ }
508
+ function readCallObjectId(value) {
509
+ return readCallId(value) ?? stringValue(value.id);
510
+ }
511
+ function readToolName(value) {
512
+ return stringValue(value.name) ?? stringValue(value.toolName) ?? stringValue(value.tool_name);
513
+ }
514
+
515
+ // src/model-evidence/request.ts
516
+ var SYSTEM_FIELDS = ["system", "systemInstruction", "system_instruction", "instructions"];
517
+ var PRIMARY_CONTAINERS = ["messages", "contents", "input"];
518
+ var ANTHROPIC_BLOCK_TYPES = /* @__PURE__ */ new Set(["thinking", "redacted_thinking", "tool_use", "tool_result"]);
519
+ function projectRequestEvidence(body) {
520
+ const evidence = { messages: [], toolDefinitions: [], diagnostics: [] };
521
+ if (body === void 0) return evidence;
522
+ if (!isRecord(body)) {
523
+ evidence.diagnostics.push(unsupported([], body));
524
+ return evidence;
525
+ }
526
+ let systemCount = 0;
527
+ for (const [field, value] of Object.entries(body)) {
528
+ if (!isSystemField(field) || value === void 0 || value === null) continue;
529
+ systemCount += projectSystemField(field, value, evidence);
530
+ }
531
+ const containers = PRIMARY_CONTAINERS.filter((name2) => isPrimaryContainer(name2, body[name2]));
532
+ const [primary, ...ambiguous] = containers;
533
+ for (const name2 of ambiguous) {
534
+ evidence.diagnostics.push({
535
+ code: "request-container-ambiguous",
536
+ severity: "warning",
537
+ source: source("request", [name2], body[name2])
538
+ });
539
+ }
540
+ if (primary === "messages") projectMessagesContainer(body.messages, evidence);
541
+ else if (primary === "contents") projectGeminiContents(body.contents, evidence);
542
+ else if (primary === "input") projectResponsesInput(body.input, evidence);
543
+ for (const [field, value] of Object.entries(body)) {
544
+ if (field === "tools" || field === "functions") collectToolDefinitions(field, value, evidence.toolDefinitions);
545
+ }
546
+ if (!primary && !systemCount) evidence.diagnostics.push(unsupported([], body));
547
+ return evidence;
548
+ }
549
+ function unsupported(path, value) {
550
+ return { code: "request-body-unsupported", severity: "warning", source: source("request", path, value) };
551
+ }
552
+ function isSystemField(field) {
553
+ return SYSTEM_FIELDS.includes(field);
554
+ }
555
+ function isPrimaryContainer(name2, value) {
556
+ if (name2 === "input") return Array.isArray(value) || typeof value === "string" || isRecord(value);
557
+ return Array.isArray(value);
558
+ }
559
+ function projectSystemField(field, value, evidence) {
560
+ const container = isRecord(value) && Array.isArray(value.parts) ? { values: value.parts, path: [field, "parts"] } : Array.isArray(value) ? { values: value, path: [field] } : void 0;
561
+ if (!container) {
562
+ evidence.messages.push(createMessage("system", normalizeContentParts(value), [field], value));
563
+ return 1;
564
+ }
565
+ for (const [index, item] of container.values.entries()) {
566
+ evidence.messages.push(createMessage("system", normalizeContentParts(item), [...container.path, String(index)], item));
567
+ }
568
+ return container.values.length;
569
+ }
570
+ function projectMessagesContainer(list, evidence) {
571
+ for (const [index, value] of list.entries()) {
572
+ const path = ["messages", String(index)];
573
+ if (!isRecord(value)) {
574
+ evidence.diagnostics.push(unsupported(path, value));
575
+ continue;
576
+ }
577
+ if (Array.isArray(value.parts)) {
578
+ projectTypedPartsMessage(value, path, evidence);
579
+ continue;
580
+ }
581
+ projectRoleMessage(value, path, evidence);
582
+ }
583
+ }
584
+ function projectRoleMessage(value, path, evidence) {
585
+ const role = stringValue(value.role)?.toLowerCase() ?? "user";
586
+ const blocks = Array.isArray(value.content) ? value.content : void 0;
587
+ const anthropic = Boolean(blocks?.some((block) => isRecord(block) && ANTHROPIC_BLOCK_TYPES.has(String(block.type))));
588
+ if (role === "assistant" || anthropic) {
589
+ projectBlockMessage(value, path, role === "assistant" ? "assistant" : normalizeRole(role), blocks, evidence);
590
+ return;
591
+ }
592
+ if (role === "tool" || role === "function" || value.tool_call_id !== void 0) {
593
+ const parts = normalizeContentParts(value.content ?? value.output);
594
+ evidence.messages.push(createToolResultMessage(parts, path, value));
595
+ return;
596
+ }
597
+ evidence.messages.push(createMessage(
598
+ normalizeRole(role),
599
+ normalizeContentParts(value.content),
600
+ path,
601
+ value,
602
+ explicitReasoning(value),
603
+ projectToolCallFields(value, path)
604
+ ));
605
+ }
606
+ function projectBlockMessage(value, path, role, blocks, evidence) {
607
+ if (!blocks) {
608
+ evidence.messages.push(createMessage(
609
+ role,
610
+ normalizeContentParts(value.content),
611
+ path,
612
+ value,
613
+ explicitReasoning(value),
614
+ projectToolCallFields(value, path)
615
+ ));
616
+ return;
617
+ }
618
+ const segments = createSegmentWriter(evidence, role, path, "content", value);
619
+ for (const text of explicitReasoning(value)) segments.addReasoning(text);
620
+ for (const call of projectToolCallFields(value, path)) segments.addToolCall(call);
621
+ for (const [index, block] of blocks.entries()) {
622
+ const blockPath = [...path, "content", String(index)];
623
+ if (!isRecord(block)) {
624
+ segments.push(index, block, normalizeContentParts(block));
625
+ continue;
626
+ }
627
+ const type = String(block.type ?? "");
628
+ if (type === "thinking" || type === "redacted_thinking") {
629
+ segments.open(index, block);
630
+ segments.addReasoning(stringValue(block.thinking) ?? stringValue(block.data));
631
+ continue;
632
+ }
633
+ if (type === "tool_use") {
634
+ segments.open(index, block);
635
+ segments.addToolCall(createToolCall(block, blockPath, block.input, { ownsCallId: true }));
636
+ continue;
637
+ }
638
+ if (type === "tool_result") {
639
+ segments.cut(createToolResultMessage(normalizeContentParts(block.content), blockPath, block, { ownsCallId: true }));
640
+ continue;
641
+ }
642
+ segments.push(index, block, normalizeContentParts([block]));
643
+ }
644
+ segments.finish();
645
+ }
646
+ function projectTypedPartsMessage(value, path, evidence) {
647
+ const role = normalizeRole(stringValue(value.role)?.toLowerCase() ?? "user");
648
+ const parts = value.parts;
649
+ const segments = createSegmentWriter(evidence, role, path, "parts", value);
650
+ for (const [index, part] of parts.entries()) {
651
+ const partPath = [...path, "parts", String(index)];
652
+ if (!isRecord(part)) {
653
+ segments.push(index, part, normalizeContentParts(part));
654
+ continue;
655
+ }
656
+ const type = stringValue(part.type)?.toLowerCase() ?? "";
657
+ if (type === "tool-result" || type === "tool-output") {
658
+ const output = part.output ?? part.result ?? part.content;
659
+ segments.cut(createToolResultMessage(
660
+ [{ kind: "text", value: normalizeToolOutput(output) }],
661
+ partPath,
662
+ part
663
+ ));
664
+ continue;
665
+ }
666
+ if (type === "reasoning" || type === "reasoning-part") {
667
+ segments.open(index, part);
668
+ segments.addReasoning(stringValue(part.text));
669
+ continue;
670
+ }
671
+ if (type === "tool-call" || type === "tool-invocation" || type === "dynamic-tool") {
672
+ const invocation = isRecord(part.toolInvocation) ? part.toolInvocation : part;
673
+ segments.open(index, part);
674
+ segments.addToolCall(createToolCall(
675
+ invocation,
676
+ partPath,
677
+ invocation.input ?? invocation.args ?? invocation.arguments
678
+ ));
679
+ continue;
680
+ }
681
+ segments.push(index, part, normalizeContentParts([part]));
682
+ }
683
+ segments.finish();
684
+ }
685
+ function projectGeminiContents(list, evidence) {
686
+ for (const [index, value] of list.entries()) {
687
+ const path = ["contents", String(index)];
688
+ if (!isRecord(value)) {
689
+ evidence.diagnostics.push(unsupported(path, value));
690
+ continue;
691
+ }
692
+ const role = value.role === "model" ? "assistant" : "user";
693
+ const parts = Array.isArray(value.parts) ? value.parts : [];
694
+ const segments = createSegmentWriter(evidence, role, path, "parts", value);
695
+ for (const [partIndex, part] of parts.entries()) {
696
+ if (!isRecord(part)) {
697
+ segments.push(partIndex, part, []);
698
+ continue;
699
+ }
700
+ if (isRecord(part.functionCall)) {
701
+ const callPath = [...path, "parts", String(partIndex), "functionCall"];
702
+ segments.open(partIndex, part);
703
+ segments.addToolCall(createToolCall(part.functionCall, callPath, part.functionCall.args ?? {}, { ownsCallId: true }));
704
+ continue;
705
+ }
706
+ if (isRecord(part.functionResponse)) {
707
+ const responsePath = [...path, "parts", String(partIndex), "functionResponse"];
708
+ const response = part.functionResponse.response ?? part.functionResponse;
709
+ segments.cut(createToolResultMessage(
710
+ normalizeContentParts(response),
711
+ responsePath,
712
+ part.functionResponse,
713
+ { ownsCallId: true }
714
+ ));
715
+ continue;
716
+ }
717
+ segments.push(partIndex, part, normalizeGeminiPart(part));
718
+ }
719
+ segments.finish();
720
+ }
721
+ }
722
+ function projectResponsesInput(input, evidence) {
723
+ if (typeof input === "string") {
724
+ evidence.messages.push(createMessage("user", normalizeContentParts(input), ["input"], input));
725
+ return;
726
+ }
727
+ if (isRecord(input)) {
728
+ projectResponsesInputItem(input, ["input"], evidence);
729
+ return;
730
+ }
731
+ if (!Array.isArray(input)) return;
732
+ for (const [index, value] of input.entries()) {
733
+ const path = ["input", String(index)];
734
+ if (!isRecord(value)) {
735
+ evidence.messages.push(createMessage("user", [{ kind: "text", value: String(value) }], path, value));
736
+ continue;
737
+ }
738
+ projectResponsesInputItem(value, path, evidence);
739
+ }
740
+ }
741
+ function projectResponsesInputItem(value, path, evidence) {
742
+ {
743
+ const type = stringValue(value.type);
744
+ if (type === "function_call") {
745
+ evidence.messages.push(createMessage(
746
+ "assistant",
747
+ [],
748
+ path,
749
+ value,
750
+ [],
751
+ [createToolCall(value, path, value.arguments ?? value.args ?? value.input)]
752
+ ));
753
+ return;
754
+ }
755
+ if (type === "function_call_output") {
756
+ evidence.messages.push(createToolResultMessage(
757
+ normalizeContentParts(value.output ?? value.result ?? value.content),
758
+ path,
759
+ value
760
+ ));
761
+ return;
762
+ }
763
+ evidence.messages.push(createMessage(
764
+ normalizeRole(stringValue(value.role)?.toLowerCase() ?? "user"),
765
+ normalizeContentParts(value.content ?? value.input),
766
+ path,
767
+ value,
768
+ explicitReasoning(value),
769
+ projectToolCallFields(value, path)
770
+ ));
771
+ }
772
+ }
773
+ function createSegmentWriter(evidence, role, path, container, value) {
774
+ const emissions = [];
775
+ let parts = [];
776
+ let reasoning = [];
777
+ let toolCalls = [];
778
+ let sources = [];
779
+ const open2 = (index, item) => {
780
+ sources.push(source("request", [...path, container, String(index)], item));
781
+ };
782
+ const flush = () => {
783
+ if (!parts.length && !reasoning.length && !toolCalls.length) return;
784
+ emissions.push({ type: "segment", parts, reasoning, toolCalls, sources });
785
+ parts = [];
786
+ reasoning = [];
787
+ toolCalls = [];
788
+ sources = [];
789
+ };
790
+ return {
791
+ addReasoning: (text) => {
792
+ if (text) reasoning.push(text);
793
+ },
794
+ addToolCall: (call) => {
795
+ if (!sources.length) sources.push(source("request", path, value));
796
+ toolCalls.push(call);
797
+ },
798
+ open: open2,
799
+ push: (index, item, incoming) => {
800
+ open2(index, item);
801
+ parts.push(...incoming);
802
+ },
803
+ cut: (message) => {
804
+ flush();
805
+ emissions.push({ type: "message", message });
806
+ },
807
+ finish: () => {
808
+ flush();
809
+ const single = emissions.length === 1 && emissions[0]?.type === "segment";
810
+ for (const emission of emissions) {
811
+ if (emission.type === "message") {
812
+ evidence.messages.push(emission.message);
813
+ continue;
814
+ }
815
+ const segmentSources = single ? [source("request", path, value)] : emission.sources;
816
+ evidence.messages.push(createMessageFromSources(
817
+ role,
818
+ emission.parts,
819
+ segmentSources[0]?.path ?? path,
820
+ segmentSources.length ? segmentSources : [source("request", path, value)],
821
+ emission.reasoning,
822
+ emission.toolCalls
823
+ ));
824
+ }
825
+ }
826
+ };
827
+ }
828
+ function normalizeRole(role) {
829
+ if (role === "assistant") return "assistant";
830
+ if (role === "system" || role === "developer") return "system";
831
+ if (role === "tool" || role === "function") return "tool";
832
+ return "user";
833
+ }
834
+ function explicitReasoning(value) {
835
+ return [value.reasoning_content, value.reasoning].filter((item) => typeof item === "string" && item.length > 0);
836
+ }
837
+ function projectToolCallFields(value, path) {
838
+ const calls = [];
839
+ for (const field of ["tool_calls", "toolCalls"]) {
840
+ const list = value[field];
841
+ if (!Array.isArray(list)) continue;
842
+ for (const [index, item] of list.entries()) {
843
+ if (!isRecord(item)) continue;
844
+ const callPath = [...path, field, String(index)];
845
+ const fn = isRecord(item.function) ? item.function : item;
846
+ calls.push(createToolCall(item, callPath, fn.arguments ?? fn.input ?? fn.args, { nameCarrier: fn, ownsCallId: true }));
847
+ }
848
+ }
849
+ if (isRecord(value.function_call)) {
850
+ calls.push(createToolCall(value.function_call, [...path, "function_call"], value.function_call.arguments));
851
+ }
852
+ return calls;
853
+ }
854
+ function createToolCall(raw, path, argumentsValue, options = {}) {
855
+ const nameCarrier = options.nameCarrier ?? raw;
856
+ const callId = options.ownsCallId ? readCallObjectId(raw) : readCallId(raw);
857
+ const name2 = readToolName(nameCarrier) ?? readToolName(raw) ?? "\u5DE5\u5177\u8C03\u7528";
858
+ const args = argumentsValue === void 0 ? void 0 : typeof argumentsValue === "string" ? argumentsValue : stringifyValue(argumentsValue);
859
+ return {
860
+ evidenceId: evidenceId("request", "tool-call", path),
861
+ name: name2,
862
+ ...callId ? { callId } : {},
863
+ ...args !== void 0 ? { arguments: args } : {},
864
+ sources: [source("request", path, raw)]
865
+ };
866
+ }
867
+ function createToolResultMessage(parts, path, raw, options = {}) {
868
+ const record = isRecord(raw) ? raw : {};
869
+ const callId = options.ownsCallId ? readCallObjectId(record) : readCallId(record);
870
+ const toolName = readToolName(record);
871
+ return {
872
+ evidenceId: evidenceId("request", "message", path),
873
+ role: "tool",
874
+ text: semanticText(parts),
875
+ contentParts: parts,
876
+ toolCalls: [],
877
+ ...callId ? { toolCallId: callId } : {},
878
+ ...toolName ? { toolName } : {},
879
+ sources: [source("request", path, raw)]
880
+ };
881
+ }
882
+ function createMessage(role, parts, path, raw, reasoning = [], toolCalls = []) {
883
+ return createMessageFromSources(role, parts, path, [source("request", path, raw)], reasoning, toolCalls);
884
+ }
885
+ function createMessageFromSources(role, parts, idPath, sources, reasoning = [], toolCalls = []) {
886
+ const reasoningText = reasoning.filter(Boolean).join("\n");
887
+ return {
888
+ evidenceId: evidenceId("request", "message", idPath),
889
+ role,
890
+ text: semanticText(parts),
891
+ contentParts: parts,
892
+ ...reasoningText ? { reasoning: reasoningText } : {},
893
+ toolCalls,
894
+ sources
895
+ };
896
+ }
897
+ function collectToolDefinitions(field, value, target) {
898
+ if (Array.isArray(value)) {
899
+ for (const [index, item] of value.entries()) {
900
+ if (!isRecord(item)) continue;
901
+ if (Array.isArray(item.functionDeclarations)) {
902
+ for (const [declarationIndex, declaration] of item.functionDeclarations.entries()) {
903
+ if (!isRecord(declaration)) continue;
904
+ target.push(createToolDefinition(declaration, [field, String(index), "functionDeclarations", String(declarationIndex)]));
905
+ }
906
+ continue;
907
+ }
908
+ const definition = isRecord(item.function) ? item.function : item;
909
+ target.push(createToolDefinition(definition, [field, String(index), ...isRecord(item.function) ? ["function"] : []]));
910
+ }
911
+ return;
912
+ }
913
+ if (!isRecord(value)) return;
914
+ for (const [name2, definition] of Object.entries(value)) {
915
+ if (!isRecord(definition)) continue;
916
+ target.push(createToolDefinition({ name: name2, ...definition }, [field, name2]));
917
+ }
918
+ }
919
+ function createToolDefinition(value, path) {
920
+ const parameters = value.parameters ?? value.input_schema ?? value.inputSchema ?? value.parametersJsonSchema;
921
+ const schema = isRecord(parameters) ? parameters : void 0;
922
+ const properties = isRecord(schema?.properties) ? schema.properties : void 0;
923
+ const requiredFields = Array.isArray(schema?.required) ? schema.required.filter((entry) => typeof entry === "string") : [];
924
+ return {
925
+ evidenceId: evidenceId("request", "tool-definition", path),
926
+ name: stringValue(value.name) ?? "\u672A\u547D\u540D\u5DE5\u5177",
927
+ description: stringValue(value.description) ?? "",
928
+ ...parameters !== void 0 ? { parameters } : {},
929
+ propertyCount: properties ? Object.keys(properties).length : 0,
930
+ requiredFields,
931
+ sources: [source("request", path, value)]
932
+ };
933
+ }
934
+
935
+ // src/model-evidence/transport.ts
936
+ var DONE_MARKER = "[DONE]";
937
+ function parseResponseTransport(raw, format) {
938
+ if (raw === void 0 || raw === "") {
939
+ return { transport: { kind: "empty" }, payloads: [], diagnostics: [] };
940
+ }
941
+ if (format === "sse") return parseSse(raw);
942
+ if (format === "text") {
943
+ return { transport: { kind: "text", value: raw }, payloads: [{ path: [], value: raw, streaming: false }], diagnostics: [] };
944
+ }
945
+ try {
946
+ const value = JSON.parse(raw);
947
+ return { transport: { kind: "json", value }, payloads: [{ path: [], value, streaming: false }], diagnostics: [] };
948
+ } catch {
949
+ return {
950
+ transport: { kind: "text", value: raw },
951
+ payloads: [],
952
+ diagnostics: format === "json" ? [{ code: "response-body-malformed", severity: "error", source: source("response", [], raw) }] : []
953
+ };
954
+ }
955
+ }
956
+ function parseSse(raw) {
957
+ const events = [];
958
+ const payloads = [];
959
+ const diagnostics = [];
960
+ for (const block of raw.replace(/\r\n/g, "\n").split(/\n\n+/)) {
961
+ if (!block.trim()) continue;
962
+ const data = [];
963
+ let event = "message";
964
+ let id;
965
+ for (const line of block.split("\n")) {
966
+ if (line.startsWith(":")) continue;
967
+ const separator = line.indexOf(":");
968
+ const field = separator < 0 ? line : line.slice(0, separator);
969
+ const value = separator < 0 ? "" : line.slice(separator + 1).replace(/^ /, "");
970
+ if (field === "data") data.push(value);
971
+ else if (field === "event") event = value || "message";
972
+ else if (field === "id") id = value;
973
+ }
974
+ const rawData = data.join("\n");
975
+ if (!rawData && event === "message" && id === void 0) continue;
976
+ const index = events.length;
977
+ let parsed = rawData;
978
+ let malformed = false;
979
+ if (rawData !== DONE_MARKER) {
980
+ try {
981
+ parsed = JSON.parse(rawData);
982
+ } catch {
983
+ malformed = true;
984
+ }
985
+ }
986
+ events.push({ ...id !== void 0 ? { id } : {}, event, data: parsed });
987
+ if (rawData === DONE_MARKER) continue;
988
+ if (malformed) {
989
+ diagnostics.push({
990
+ code: "response-body-malformed",
991
+ severity: "error",
992
+ source: source("response", [String(index), "data"], rawData)
993
+ });
994
+ continue;
995
+ }
996
+ payloads.push({ path: [String(index), "data"], value: parsed, streaming: true });
997
+ }
998
+ return { transport: { kind: "sse", value: events }, payloads, diagnostics };
999
+ }
1000
+
1001
+ // src/model-evidence/usage.ts
1002
+ function normalizeUsageCandidate(usage2) {
1003
+ if (!usage2) return void 0;
1004
+ const cacheReadTokens = readUsageNumber(usage2, ["cache_read_input_tokens"]) ?? 0;
1005
+ const cacheCreationTokens = readUsageNumber(usage2, ["cache_creation_input_tokens"]) ?? 0;
1006
+ const cachedTokens = firstUsageNumber(usage2, [
1007
+ ["prompt_tokens_details", "cached_tokens"],
1008
+ ["input_tokens_details", "cached_tokens"],
1009
+ ["cachedContentTokenCount"]
1010
+ ]) ?? (cacheReadTokens + cacheCreationTokens || void 0);
1011
+ const reasoningTokens = firstUsageNumber(usage2, [
1012
+ ["completion_tokens_details", "reasoning_tokens"],
1013
+ ["output_tokens_details", "reasoning_tokens"],
1014
+ ["thoughtsTokenCount"],
1015
+ ["reasoning_tokens"]
1016
+ ]);
1017
+ let inputTokens = firstUsageNumber(usage2, [
1018
+ ["prompt_tokens"],
1019
+ ["promptTokenCount"]
1020
+ ]);
1021
+ if (inputTokens === void 0) {
1022
+ const uncachedInputTokens = readUsageNumber(usage2, ["input_tokens"]);
1023
+ inputTokens = uncachedInputTokens === void 0 ? void 0 : uncachedInputTokens + cacheReadTokens + cacheCreationTokens;
1024
+ }
1025
+ let outputTokens = firstUsageNumber(usage2, [
1026
+ ["completion_tokens"],
1027
+ ["output_tokens"],
1028
+ ["candidatesTokenCount"]
1029
+ ]);
1030
+ if (outputTokens !== void 0 && reasoningTokens !== void 0 && "completion_tokens" in usage2) {
1031
+ outputTokens = Math.max(0, outputTokens - reasoningTokens);
1032
+ }
1033
+ const reportedTotal = firstUsageNumber(usage2, [
1034
+ ["total_tokens"],
1035
+ ["totalTokenCount"]
1036
+ ]);
1037
+ if ((outputTokens === void 0 || outputTokens === 0) && inputTokens !== void 0 && reportedTotal !== void 0) {
1038
+ const impliedVisible = reportedTotal - inputTokens - (reasoningTokens ?? 0);
1039
+ if (impliedVisible > 0) outputTokens = impliedVisible;
1040
+ }
1041
+ const totalTokens = reportedTotal ?? sumUsageTokens(inputTokens, outputTokens, reasoningTokens);
1042
+ const normalized = { inputTokens, outputTokens, reasoningTokens, cachedTokens, totalTokens };
1043
+ return Object.values(normalized).some((value) => value !== void 0) ? normalized : void 0;
1044
+ }
1045
+ function mergeUsage(current, incoming) {
1046
+ if (!current) return { ...incoming };
1047
+ const merged = { ...current };
1048
+ for (const [key, value] of Object.entries(incoming)) {
1049
+ if (typeof value === "number" && Number.isFinite(value)) {
1050
+ const existing = merged[key];
1051
+ merged[key] = typeof existing === "number" ? Math.max(existing, value) : value;
1052
+ continue;
1053
+ }
1054
+ if (isRecord(value)) {
1055
+ const existing = merged[key];
1056
+ merged[key] = isRecord(existing) ? mergeUsage(existing, value) : { ...value };
1057
+ continue;
1058
+ }
1059
+ if (value !== void 0) merged[key] = value;
1060
+ }
1061
+ return merged;
1062
+ }
1063
+ function firstUsageNumber(usage2, paths) {
1064
+ for (const path of paths) {
1065
+ const value = readUsageNumber(usage2, path);
1066
+ if (value !== void 0) return value;
1067
+ }
1068
+ }
1069
+ function readUsageNumber(usage2, path) {
1070
+ let value = usage2;
1071
+ for (const key of path) {
1072
+ if (!isRecord(value)) return void 0;
1073
+ value = value[key];
1074
+ }
1075
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
1076
+ }
1077
+ function sumUsageTokens(...values) {
1078
+ const available = values.filter((value) => value !== void 0);
1079
+ return available.length ? available.reduce((total, value) => total + value, 0) : void 0;
1080
+ }
1081
+
1082
+ // src/model-evidence/response.ts
1083
+ var ANTHROPIC_EVENT_TYPES = /* @__PURE__ */ new Set([
1084
+ "message",
1085
+ "message_start",
1086
+ "message_delta",
1087
+ "message_stop",
1088
+ "content_block_start",
1089
+ "content_block_delta",
1090
+ "content_block_stop",
1091
+ "ping"
1092
+ ]);
1093
+ var AI_SDK_PART_TYPES = /* @__PURE__ */ new Set([
1094
+ "text",
1095
+ "text-start",
1096
+ "text-delta",
1097
+ "text-end",
1098
+ "reasoning",
1099
+ "reasoning-start",
1100
+ "reasoning-delta",
1101
+ "reasoning-end",
1102
+ "tool-input-start",
1103
+ "tool-input-delta",
1104
+ "tool-call",
1105
+ "tool-result",
1106
+ "tool-output",
1107
+ "finish",
1108
+ "finish-step",
1109
+ "error"
1110
+ ]);
1111
+ function projectResponseEvidence(raw, format) {
1112
+ const { transport, payloads, diagnostics } = parseResponseTransport(raw, format);
1113
+ const collector = createCollector();
1114
+ for (const payload of payloads) projectPayload(payload, payload.value, payload.path, collector, diagnostics);
1115
+ return { transport, events: collector.events(), diagnostics };
1116
+ }
1117
+ function createCollector() {
1118
+ const order = [];
1119
+ const byKey = /* @__PURE__ */ new Map();
1120
+ const dedupe = /* @__PURE__ */ new Map();
1121
+ return {
1122
+ collect: (draft) => {
1123
+ const key = draft.dedupeKey !== void 0 ? dedupe.get(draft.dedupeKey) ?? `${draft.kind}|${draft.slot.join(".")}` : `${draft.kind}|${draft.slot.join(".")}`;
1124
+ if (draft.dedupeKey !== void 0 && !dedupe.has(draft.dedupeKey)) dedupe.set(draft.dedupeKey, key);
1125
+ let event = byKey.get(key);
1126
+ if (!event) {
1127
+ event = { kind: draft.kind, slot: draft.slot, sources: [], sourceKeys: /* @__PURE__ */ new Set() };
1128
+ byKey.set(key, event);
1129
+ order.push(key);
1130
+ }
1131
+ const sourceKey = draft.source.path.join(".");
1132
+ if (!event.sourceKeys.has(sourceKey)) {
1133
+ event.sourceKeys.add(sourceKey);
1134
+ event.sources.push(draft.source);
1135
+ }
1136
+ if (draft.text !== void 0) {
1137
+ event.text = accumulate(event.text, draft.text, draft.textAccumulation ?? "snapshot");
1138
+ }
1139
+ if (draft.arguments !== void 0) {
1140
+ event.arguments = accumulate(event.arguments, draft.arguments, draft.argumentsAccumulation ?? "snapshot");
1141
+ }
1142
+ if (draft.name && !event.name) event.name = draft.name;
1143
+ if (draft.callId && !event.callId) event.callId = draft.callId;
1144
+ if (draft.usage) event.usage = mergeUsage(event.usage, draft.usage);
1145
+ },
1146
+ events: () => order.flatMap((key) => {
1147
+ const event = byKey.get(key);
1148
+ if (!event) return [];
1149
+ const normalizedUsage = event.usage ? normalizeUsageCandidate(event.usage) : void 0;
1150
+ return [{
1151
+ evidenceId: evidenceId("response", event.kind, event.slot),
1152
+ kind: event.kind,
1153
+ ...event.text !== void 0 ? { text: event.text } : {},
1154
+ ...event.name !== void 0 ? { name: event.name } : {},
1155
+ ...event.callId !== void 0 ? { callId: event.callId } : {},
1156
+ ...event.arguments !== void 0 ? { arguments: event.arguments } : {},
1157
+ ...event.usage !== void 0 ? { usage: event.usage } : {},
1158
+ ...normalizedUsage ? { normalizedUsage } : {},
1159
+ sources: event.sources
1160
+ }];
1161
+ })
1162
+ };
1163
+ }
1164
+ function accumulate(existing, incoming, mode) {
1165
+ if (existing === void 0) return incoming;
1166
+ if (mode === "delta") return `${existing}${incoming}`;
1167
+ if (incoming === existing) return existing;
1168
+ return incoming;
1169
+ }
1170
+ function projectPayload(payload, value, path, collector, diagnostics) {
1171
+ if (typeof value === "string") {
1172
+ if (!value) return;
1173
+ collector.collect({
1174
+ kind: "content",
1175
+ slot: ["body"],
1176
+ text: value,
1177
+ textAccumulation: "snapshot",
1178
+ source: source("response", path, value)
1179
+ });
1180
+ return;
1181
+ }
1182
+ if (!isRecord(value)) {
1183
+ unknownPayload(value, path, collector, diagnostics, "response-payload-unsupported");
1184
+ return;
1185
+ }
1186
+ const context = { path, streaming: payload.streaming, collect: collector.collect };
1187
+ const matched = ADAPTERS.filter((adapter) => adapter.matches(value));
1188
+ if (matched.length > 1) {
1189
+ unknownPayload(value, path, collector, diagnostics, "response-payload-ambiguous");
1190
+ collectUsage(value, path, collector);
1191
+ return;
1192
+ }
1193
+ if (matched.length === 1) {
1194
+ matched[0].project(value, context, payload, collector, diagnostics);
1195
+ collectUsage(value, path, collector);
1196
+ return;
1197
+ }
1198
+ const usageOnly = collectUsage(value, path, collector);
1199
+ if (!usageOnly) unknownPayload(value, path, collector, diagnostics, "response-payload-unsupported");
1200
+ }
1201
+ function unknownPayload(value, path, collector, diagnostics, code) {
1202
+ collector.collect({ kind: "unknown", slot: ["unknown", ...path], source: source("response", path, value) });
1203
+ diagnostics.push({ code, severity: "warning", source: source("response", path, value) });
1204
+ }
1205
+ function collectUsage(value, path, collector) {
1206
+ let found = false;
1207
+ for (const field of ["usage", "usageMetadata"]) {
1208
+ const usage2 = value[field];
1209
+ if (!isRecord(usage2)) continue;
1210
+ found = true;
1211
+ collector.collect({
1212
+ kind: "usage",
1213
+ slot: ["usage"],
1214
+ usage: usage2,
1215
+ source: source("response", [...path, field], usage2)
1216
+ });
1217
+ }
1218
+ return found;
1219
+ }
1220
+ var openAiChatAdapter = {
1221
+ matches: (payload) => Array.isArray(payload.choices),
1222
+ project: (payload, context) => {
1223
+ for (const [index, choice] of payload.choices.entries()) {
1224
+ if (!isRecord(choice)) continue;
1225
+ const choicePath = [...context.path, "choices", String(index)];
1226
+ const container = isRecord(choice.message) ? { field: "message", value: choice.message, accumulation: "snapshot" } : isRecord(choice.delta) ? { field: "delta", value: choice.delta, accumulation: "delta" } : void 0;
1227
+ if (container) {
1228
+ const base = [...choicePath, container.field];
1229
+ for (const field of ["reasoning_content", "reasoning", "reasoning_text"]) {
1230
+ const text = stringValue(container.value[field]);
1231
+ if (!text) continue;
1232
+ context.collect({
1233
+ kind: "reasoning",
1234
+ slot: ["choices", String(index), "reasoning"],
1235
+ text,
1236
+ textAccumulation: container.accumulation,
1237
+ source: source("response", [...base, field], container.value[field])
1238
+ });
1239
+ }
1240
+ addTextValue(context, container.value.content, [...base, "content"], ["choices", String(index), "content"], "content", container.accumulation);
1241
+ if (Array.isArray(container.value.tool_calls)) {
1242
+ for (const [callIndex, call] of container.value.tool_calls.entries()) {
1243
+ if (!isRecord(call)) continue;
1244
+ const fn = isRecord(call.function) ? call.function : call;
1245
+ const slotKey = typeof call.index === "number" ? String(call.index) : String(callIndex);
1246
+ context.collect({
1247
+ kind: "tool-call",
1248
+ slot: ["choices", String(index), "tool_calls", slotKey],
1249
+ ...readToolName(fn) ? { name: readToolName(fn) } : {},
1250
+ ...readCallObjectId(call) ? { callId: readCallObjectId(call) } : {},
1251
+ ...fn.arguments === void 0 ? {} : { arguments: stringifyArguments(fn.arguments), argumentsAccumulation: container.accumulation },
1252
+ source: source("response", [...base, "tool_calls", String(callIndex)], call)
1253
+ });
1254
+ }
1255
+ }
1256
+ if (isRecord(container.value.function_call)) {
1257
+ const call = container.value.function_call;
1258
+ context.collect({
1259
+ kind: "tool-call",
1260
+ slot: ["choices", String(index), "function_call"],
1261
+ ...readToolName(call) ? { name: readToolName(call) } : {},
1262
+ ...call.arguments === void 0 ? {} : { arguments: stringifyArguments(call.arguments), argumentsAccumulation: container.accumulation },
1263
+ source: source("response", [...base, "function_call"], call)
1264
+ });
1265
+ }
1266
+ }
1267
+ for (const field of ["finish_reason", "native_finish_reason"]) {
1268
+ const finish = stringValue(choice[field]);
1269
+ if (!finish) continue;
1270
+ context.collect({
1271
+ kind: "finish-reason",
1272
+ slot: ["choices", String(index), field],
1273
+ text: finish,
1274
+ source: source("response", [...choicePath, field], choice[field])
1275
+ });
1276
+ }
1277
+ }
1278
+ }
1279
+ };
1280
+ var anthropicAdapter = {
1281
+ matches: (payload) => Array.isArray(payload.content) || isRecord(payload.content_block) || isRecord(payload.delta) && typeof payload.delta.type === "string" || ANTHROPIC_EVENT_TYPES.has(String(payload.type)) || typeof payload.stop_reason === "string",
1282
+ project: (payload, context, origin, collector, diagnostics) => {
1283
+ if (isRecord(payload.message)) {
1284
+ projectPayload(origin, payload.message, [...context.path, "message"], collector, diagnostics);
1285
+ }
1286
+ if (Array.isArray(payload.content)) {
1287
+ for (const [index, block] of payload.content.entries()) {
1288
+ if (!isRecord(block)) continue;
1289
+ const blockPath = [...context.path, "content", String(index)];
1290
+ const slot = ["content", String(index)];
1291
+ const type = String(block.type ?? "");
1292
+ if (type === "text") {
1293
+ const text = stringValue(block.text);
1294
+ if (text) context.collect({ kind: "content", slot, text, source: source("response", [...blockPath, "text"], block.text) });
1295
+ continue;
1296
+ }
1297
+ if (type === "thinking") {
1298
+ const text = stringValue(block.thinking);
1299
+ if (text) context.collect({ kind: "reasoning", slot, text, source: source("response", [...blockPath, "thinking"], block.thinking) });
1300
+ continue;
1301
+ }
1302
+ if (type === "tool_use") {
1303
+ context.collect({
1304
+ kind: "tool-call",
1305
+ slot,
1306
+ ...readToolName(block) ? { name: readToolName(block) } : {},
1307
+ ...stringValue(block.id) ? { callId: stringValue(block.id) } : {},
1308
+ ...block.input === void 0 ? {} : { arguments: stringifyArguments(block.input) },
1309
+ source: source("response", blockPath, block)
1310
+ });
1311
+ continue;
1312
+ }
1313
+ if (type === "tool_result") {
1314
+ collectToolResult(context, block, blockPath, slot, block.content);
1315
+ }
1316
+ }
1317
+ }
1318
+ if (isRecord(payload.content_block)) {
1319
+ const blockSlot = ["content_block", typeof payload.index === "number" ? String(payload.index) : "0"];
1320
+ const block = payload.content_block;
1321
+ if (String(block.type ?? "") === "tool_use") {
1322
+ context.collect({
1323
+ kind: "tool-call",
1324
+ slot: blockSlot,
1325
+ ...readToolName(block) ? { name: readToolName(block) } : {},
1326
+ ...stringValue(block.id) ? { callId: stringValue(block.id) } : {},
1327
+ source: source("response", [...context.path, "content_block"], block)
1328
+ });
1329
+ }
1330
+ }
1331
+ if (isRecord(payload.delta)) {
1332
+ const delta = payload.delta;
1333
+ const blockSlot = ["content_block", typeof payload.index === "number" ? String(payload.index) : "0"];
1334
+ const deltaPath = [...context.path, "delta"];
1335
+ if (delta.type === "text_delta" && stringValue(delta.text)) {
1336
+ context.collect({ kind: "content", slot: blockSlot, text: String(delta.text), textAccumulation: "delta", source: source("response", [...deltaPath, "text"], delta.text) });
1337
+ }
1338
+ if (delta.type === "thinking_delta" && stringValue(delta.thinking)) {
1339
+ context.collect({ kind: "reasoning", slot: blockSlot, text: String(delta.thinking), textAccumulation: "delta", source: source("response", [...deltaPath, "thinking"], delta.thinking) });
1340
+ }
1341
+ if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
1342
+ context.collect({ kind: "tool-call", slot: blockSlot, arguments: delta.partial_json, argumentsAccumulation: "delta", source: source("response", [...deltaPath, "partial_json"], delta.partial_json) });
1343
+ }
1344
+ const stop2 = stringValue(delta.stop_reason);
1345
+ if (stop2) context.collect({ kind: "finish-reason", slot: ["stop_reason"], text: stop2, source: source("response", [...deltaPath, "stop_reason"], delta.stop_reason) });
1346
+ }
1347
+ const stop = stringValue(payload.stop_reason);
1348
+ if (stop) {
1349
+ context.collect({ kind: "finish-reason", slot: ["stop_reason"], text: stop, source: source("response", [...context.path, "stop_reason"], payload.stop_reason) });
1350
+ }
1351
+ }
1352
+ };
1353
+ var geminiAdapter = {
1354
+ matches: (payload) => Array.isArray(payload.candidates),
1355
+ project: (payload, context) => {
1356
+ for (const [index, candidate] of payload.candidates.entries()) {
1357
+ if (!isRecord(candidate)) continue;
1358
+ const candidatePath = [...context.path, "candidates", String(index)];
1359
+ if (isRecord(candidate.content) && Array.isArray(candidate.content.parts)) {
1360
+ for (const [partIndex, part] of candidate.content.parts.entries()) {
1361
+ if (!isRecord(part)) continue;
1362
+ const partPath = [...candidatePath, "content", "parts", String(partIndex)];
1363
+ const reasoning = part.thought === true;
1364
+ if (typeof part.text === "string" && part.text) {
1365
+ context.collect({
1366
+ kind: reasoning ? "reasoning" : "content",
1367
+ slot: context.streaming ? ["candidates", String(index), reasoning ? "thought" : "content"] : ["candidates", String(index), "parts", String(partIndex)],
1368
+ text: part.text,
1369
+ textAccumulation: context.streaming ? "delta" : "snapshot",
1370
+ source: source("response", [...partPath, "text"], part.text)
1371
+ });
1372
+ }
1373
+ if (isRecord(part.functionCall)) {
1374
+ const call = part.functionCall;
1375
+ context.collect({
1376
+ kind: "tool-call",
1377
+ slot: ["candidates", String(index), "parts", String(partIndex)],
1378
+ ...readToolName(call) ? { name: readToolName(call) } : {},
1379
+ ...stringValue(call.id) ? { callId: stringValue(call.id) } : {},
1380
+ ...call.args === void 0 ? {} : { arguments: stringifyArguments(call.args) },
1381
+ source: source("response", [...partPath, "functionCall"], call)
1382
+ });
1383
+ }
1384
+ }
1385
+ }
1386
+ const finish = stringValue(candidate.finishReason);
1387
+ if (finish) {
1388
+ context.collect({
1389
+ kind: "finish-reason",
1390
+ slot: ["candidates", String(index), "finishReason"],
1391
+ text: finish,
1392
+ source: source("response", [...candidatePath, "finishReason"], candidate.finishReason)
1393
+ });
1394
+ }
1395
+ }
1396
+ }
1397
+ };
1398
+ var responsesAdapter = {
1399
+ matches: (payload) => Array.isArray(payload.output) || typeof payload.type === "string" && payload.type.startsWith("response."),
1400
+ project: (payload, context, origin, collector, diagnostics) => {
1401
+ const type = stringValue(payload.type);
1402
+ if (type) {
1403
+ if (type.endsWith(".output_text.delta") && stringValue(payload.delta)) {
1404
+ context.collect({ kind: "content", slot: ["output_text"], text: String(payload.delta), textAccumulation: "delta", source: source("response", [...context.path, "delta"], payload.delta) });
1405
+ }
1406
+ if (type.endsWith(".reasoning_summary_text.delta") && stringValue(payload.delta)) {
1407
+ context.collect({ kind: "reasoning", slot: ["reasoning_summary_text"], text: String(payload.delta), textAccumulation: "delta", source: source("response", [...context.path, "delta"], payload.delta) });
1408
+ }
1409
+ if (type.endsWith(".function_call_arguments.delta")) {
1410
+ const itemId = stringValue(payload.item_id);
1411
+ context.collect({
1412
+ kind: "tool-call",
1413
+ slot: ["item", itemId ?? "function_call"],
1414
+ ...readToolName(payload) ? { name: readToolName(payload) } : {},
1415
+ ...itemId ? { callId: itemId } : {},
1416
+ ...typeof payload.delta === "string" ? { arguments: payload.delta, argumentsAccumulation: "delta" } : {},
1417
+ source: source("response", [...context.path, "delta"], payload.delta)
1418
+ });
1419
+ }
1420
+ if (isRecord(payload.response)) {
1421
+ projectPayload(origin, payload.response, [...context.path, "response"], collector, diagnostics);
1422
+ }
1423
+ }
1424
+ if (!Array.isArray(payload.output)) return;
1425
+ for (const [index, item] of payload.output.entries()) {
1426
+ if (!isRecord(item)) continue;
1427
+ const itemPath = [...context.path, "output", String(index)];
1428
+ const slot = ["output", String(index)];
1429
+ const itemType = stringValue(item.type);
1430
+ if (itemType === "message") {
1431
+ addTextValue(context, item.content, itemPath, slot, "content", "snapshot");
1432
+ continue;
1433
+ }
1434
+ if (itemType === "reasoning") {
1435
+ addTextValue(context, item.summary, itemPath, slot, "reasoning", "snapshot");
1436
+ continue;
1437
+ }
1438
+ if (itemType === "function_call") {
1439
+ context.collect({
1440
+ kind: "tool-call",
1441
+ slot,
1442
+ ...readToolName(item) ? { name: readToolName(item) } : {},
1443
+ ...stringValue(item.call_id) ? { callId: stringValue(item.call_id) } : {},
1444
+ ...item.arguments === void 0 ? {} : { arguments: stringifyArguments(item.arguments) },
1445
+ source: source("response", itemPath, item)
1446
+ });
1447
+ continue;
1448
+ }
1449
+ if (itemType === "function_call_output") {
1450
+ collectToolResult(context, item, itemPath, slot, item.output ?? item.result ?? item.content);
1451
+ }
1452
+ }
1453
+ }
1454
+ };
1455
+ var aiSdkAdapter = {
1456
+ matches: (payload) => typeof payload.type === "string" && AI_SDK_PART_TYPES.has(payload.type),
1457
+ project: (payload, context) => {
1458
+ const type = String(payload.type);
1459
+ const id = stringValue(payload.id) ?? stringValue(payload.toolCallId) ?? "0";
1460
+ if (type === "text" || type === "text-delta") {
1461
+ const text = stringValue(payload.text) ?? stringValue(payload.delta);
1462
+ if (text) {
1463
+ context.collect({
1464
+ kind: "content",
1465
+ slot: ["text", id],
1466
+ text,
1467
+ textAccumulation: type === "text-delta" ? "delta" : "snapshot",
1468
+ source: source("response", context.path, payload)
1469
+ });
1470
+ }
1471
+ return;
1472
+ }
1473
+ if (type === "reasoning" || type === "reasoning-delta") {
1474
+ const text = stringValue(payload.text) ?? stringValue(payload.delta);
1475
+ if (text) {
1476
+ context.collect({
1477
+ kind: "reasoning",
1478
+ slot: ["reasoning", id],
1479
+ text,
1480
+ textAccumulation: type === "reasoning-delta" ? "delta" : "snapshot",
1481
+ source: source("response", context.path, payload)
1482
+ });
1483
+ }
1484
+ return;
1485
+ }
1486
+ if (type === "tool-input-delta") {
1487
+ context.collect({
1488
+ kind: "tool-call",
1489
+ slot: ["tool", id],
1490
+ ...typeof payload.delta === "string" ? { arguments: payload.delta, argumentsAccumulation: "delta" } : {},
1491
+ source: source("response", context.path, payload)
1492
+ });
1493
+ return;
1494
+ }
1495
+ if (type === "tool-input-start" || type === "tool-call") {
1496
+ const args = payload.input ?? payload.args ?? payload.arguments;
1497
+ context.collect({
1498
+ kind: "tool-call",
1499
+ slot: ["tool", id],
1500
+ ...readToolName(payload) ? { name: readToolName(payload) } : {},
1501
+ ...readCallId(payload) ? { callId: readCallId(payload) } : {},
1502
+ ...args === void 0 ? {} : { arguments: stringifyArguments(args) },
1503
+ source: source("response", context.path, payload)
1504
+ });
1505
+ return;
1506
+ }
1507
+ if (type === "tool-result" || type === "tool-output") {
1508
+ collectToolResult(context, payload, context.path, ["tool-result", id], payload.output ?? payload.result ?? payload.content);
1509
+ return;
1510
+ }
1511
+ if (type === "finish" || type === "finish-step") {
1512
+ const reason = stringValue(payload.finishReason) ?? stringValue(payload.finish_reason);
1513
+ if (reason) {
1514
+ context.collect({ kind: "finish-reason", slot: ["finishReason"], text: reason, source: source("response", context.path, payload) });
1515
+ }
1516
+ }
1517
+ }
1518
+ };
1519
+ var ADAPTERS = [
1520
+ openAiChatAdapter,
1521
+ responsesAdapter,
1522
+ anthropicAdapter,
1523
+ geminiAdapter,
1524
+ aiSdkAdapter
1525
+ ];
1526
+ function collectToolResult(context, raw, path, slot, output) {
1527
+ const callId = readCallId(raw);
1528
+ const name2 = readToolName(raw);
1529
+ const text = normalizeToolOutput(output);
1530
+ context.collect({
1531
+ kind: "tool-result",
1532
+ slot,
1533
+ text,
1534
+ ...name2 ? { name: name2 } : {},
1535
+ ...callId ? { callId } : {},
1536
+ dedupeKey: JSON.stringify(["tool-result", callId ?? "", name2 ?? "", text]),
1537
+ source: source("response", path, raw)
1538
+ });
1539
+ }
1540
+ function addTextValue(context, value, path, slot, kind, accumulation) {
1541
+ if (typeof value === "string") {
1542
+ if (value) context.collect({ kind, slot, text: value, textAccumulation: accumulation, source: source("response", path, value) });
1543
+ return;
1544
+ }
1545
+ if (!Array.isArray(value)) return;
1546
+ const text = value.flatMap((item) => {
1547
+ if (typeof item === "string") return [item];
1548
+ if (!isRecord(item)) return [];
1549
+ const part = stringValue(item.text) ?? stringValue(item.output_text) ?? stringValue(item.content);
1550
+ return part ? [part] : [];
1551
+ }).join("");
1552
+ if (text) context.collect({ kind, slot, text, textAccumulation: accumulation, source: source("response", path, value) });
1553
+ }
1554
+ function stringifyArguments(value) {
1555
+ return typeof value === "string" ? value : stringifyValue(value);
1556
+ }
1557
+
1558
+ // src/model-evidence/metrics.ts
1559
+ function countMessageCharacters(message) {
1560
+ const text = message.text || message.contentParts.map((part) => part.value).join("\n");
1561
+ return text.length + (message.reasoning?.length ?? 0);
1562
+ }
1563
+ function countToolCallCharacters(call) {
1564
+ return call.arguments?.length ?? serializedLength(call.sources[0]?.value);
1565
+ }
1566
+ function countToolDefinitionCharacters(definition) {
1567
+ return serializedLength(definition.sources[0]?.value);
1568
+ }
1569
+ function serializedLength(value) {
1570
+ if (value === void 0 || value === null) return 0;
1571
+ if (typeof value === "string") return value.length;
1572
+ try {
1573
+ return JSON.stringify(value).length;
1574
+ } catch {
1575
+ return String(value).length;
1576
+ }
1577
+ }
1578
+
1579
+ // src/model-evidence/index.ts
1580
+ function projectModelEvidence(slice) {
1581
+ const request = projectRequestEvidence(slice.requestBody);
1582
+ const response = projectResponseEvidence(slice.responseBodyRaw, slice.responseBodyFormat);
1583
+ return {
1584
+ requestMessages: request.messages,
1585
+ toolDefinitions: request.toolDefinitions,
1586
+ responseEvents: response.events,
1587
+ responseTransport: response.transport,
1588
+ diagnostics: [...request.diagnostics, ...response.diagnostics]
1589
+ };
1590
+ }
1591
+
1592
+ // src/model-request-variables.ts
1593
+ var import_yaml2 = require("yaml");
1594
+
1595
+ // src/presets/evidence-match.ts
1596
+ var MAX_VARIANTS = 256;
1597
+ var MAX_LITERAL_PLACEMENTS = 256;
1598
+ function matchPresetExpressionEvidence(input) {
1599
+ if (input.expression.kind === "control") return { status: "unsupported", reason: "control-tag" };
1600
+ const currentExpression = input.document.expressions.find((expression) => sameExpression(expression, input.expression));
1601
+ if (!currentExpression) return { status: "stale" };
1602
+ const field = input.document.templateFields.find((candidate) => samePath(candidate.path, currentExpression.path));
1603
+ const snapshotTemplate = input.snapshot.templates.find((candidate) => samePath(candidate.path, currentExpression.path));
1604
+ if (!field || !snapshotTemplate) return { status: "unsupported", reason: "template-field" };
1605
+ if (field.value !== snapshotTemplate.template) return { status: "stale" };
1606
+ const fieldExpressions = input.document.expressions.filter((expression) => samePath(expression.path, currentExpression.path));
1607
+ const expressionIndex = fieldExpressions.findIndex((expression) => sameExpression(expression, currentExpression));
1608
+ if (expressionIndex < 0) return { status: "stale" };
1609
+ const parsed = parseTemplate(field.value);
1610
+ if (!parsed.nodes) return { status: "ambiguous" };
1611
+ let ambiguous = false;
1612
+ const matches = input.evidence.requestMessages.filter((message) => message.role === snapshotTemplate.role).flatMap((message) => {
1613
+ const result = matchTemplateInText(parsed.nodes, expressionIndex, message.text);
1614
+ ambiguous ||= result.ambiguous;
1615
+ return result.ranges.map((range) => ({ evidenceId: message.evidenceId, range }));
1616
+ });
1617
+ if (ambiguous) return { status: "ambiguous" };
1618
+ const unique = deduplicateMatches(matches);
1619
+ if (!unique.length) return { status: "not-observed" };
1620
+ return unique.length === 1 ? { status: "matched", ...unique[0] } : { status: "ambiguous" };
1621
+ }
1622
+ function matchTemplateInText(nodes, targetExpressionIndex, text) {
1623
+ const expanded = expandNodes(nodes, text);
1624
+ const ranges = [];
1625
+ let ambiguous = expanded.truncated;
1626
+ let observedWithoutTarget = false;
1627
+ for (const parts of expanded.variants) {
1628
+ const linear = linearize(parts);
1629
+ const result = matchLinearTemplate(linear, targetExpressionIndex, text);
1630
+ ranges.push(...result.ranges);
1631
+ ambiguous ||= result.ambiguous;
1632
+ observedWithoutTarget ||= Boolean(result.observed && !linear.expressions.includes(targetExpressionIndex));
1633
+ }
1634
+ const uniqueRanges = deduplicateRanges(ranges);
1635
+ if (uniqueRanges.length && observedWithoutTarget) ambiguous = true;
1636
+ return { ranges: uniqueRanges, ambiguous };
1637
+ }
1638
+ function matchLinearTemplate(template, targetExpressionIndex, text) {
1639
+ const enumerated = enumerateLiteralPlacements(template.literals, text);
1640
+ const targetPositions = template.expressions.flatMap((expressionIndex, index) => expressionIndex === targetExpressionIndex ? [index] : []);
1641
+ const results = [];
1642
+ let ambiguous = enumerated.truncated;
1643
+ for (const placement of enumerated.placements) {
1644
+ if (!targetPositions.length) continue;
1645
+ if (targetPositions.length > 1) ambiguous = true;
1646
+ for (const targetPosition of targetPositions) {
1647
+ let runStart = targetPosition;
1648
+ let runEnd = targetPosition;
1649
+ while (runStart > 0 && template.literals[runStart] === "") runStart -= 1;
1650
+ while (runEnd + 1 < template.expressions.length && template.literals[runEnd + 1] === "") runEnd += 1;
1651
+ const runGapStart = placement[runStart].end;
1652
+ const runGapEnd = placement[runEnd + 1].start;
1653
+ const runLength = runEnd - runStart + 1;
1654
+ if (runLength > 1 && runGapEnd > runGapStart) {
1655
+ ambiguous = true;
1656
+ continue;
1657
+ }
1658
+ results.push(runLength > 1 ? { start: runGapStart, end: runGapStart } : { start: placement[targetPosition].end, end: placement[targetPosition + 1].start });
1659
+ }
1660
+ }
1661
+ return { ranges: deduplicateRanges(results), ambiguous, observed: enumerated.placements.length > 0 };
1662
+ }
1663
+ function parseTemplate(template) {
1664
+ const lexemes = tokenizeTemplate(template);
1665
+ let position = 0;
1666
+ const parseSequence = (stops) => {
1667
+ const nodes2 = [];
1668
+ while (position < lexemes.length) {
1669
+ const lexeme = lexemes[position];
1670
+ if (lexeme.kind === "literal") {
1671
+ nodes2.push(lexeme);
1672
+ position += 1;
1673
+ continue;
1674
+ }
1675
+ if (lexeme.kind === "value") {
1676
+ nodes2.push(lexeme);
1677
+ position += 1;
1678
+ continue;
1679
+ }
1680
+ if (stops.has(lexeme.control)) return nodes2;
1681
+ if (lexeme.control === "if") {
1682
+ position += 1;
1683
+ const branches = [];
1684
+ const first = parseSequence(/* @__PURE__ */ new Set(["elseif", "else", "/if"]));
1685
+ if (!first) return;
1686
+ branches.push(first);
1687
+ while (controlAt(lexemes, position) === "elseif") {
1688
+ position += 1;
1689
+ const branch = parseSequence(/* @__PURE__ */ new Set(["elseif", "else", "/if"]));
1690
+ if (!branch) return;
1691
+ branches.push(branch);
1692
+ }
1693
+ if (controlAt(lexemes, position) === "else") {
1694
+ position += 1;
1695
+ const branch = parseSequence(/* @__PURE__ */ new Set(["/if"]));
1696
+ if (!branch) return;
1697
+ branches.push(branch);
1698
+ } else {
1699
+ branches.push([]);
1700
+ }
1701
+ if (controlAt(lexemes, position) !== "/if") return;
1702
+ position += 1;
1703
+ nodes2.push({ kind: "conditional", branches });
1704
+ continue;
1705
+ }
1706
+ if (lexeme.control === "for" || lexeme.control === "while" || lexeme.control === "repeat") {
1707
+ const close = `/${lexeme.control}`;
1708
+ position += 1;
1709
+ const body = parseSequence(/* @__PURE__ */ new Set([close]));
1710
+ if (!body || controlAt(lexemes, position) !== close) return;
1711
+ position += 1;
1712
+ nodes2.push({ kind: "loop", body });
1713
+ continue;
1714
+ }
1715
+ return;
1716
+ }
1717
+ return nodes2;
1718
+ };
1719
+ const nodes = parseSequence(/* @__PURE__ */ new Set());
1720
+ return nodes && position === lexemes.length ? { nodes } : {};
1721
+ }
1722
+ function controlAt(lexemes, position) {
1723
+ const lexeme = lexemes[position];
1724
+ return lexeme?.kind === "control" ? lexeme.control : void 0;
1725
+ }
1726
+ function expandNodes(nodes, text) {
1727
+ let variants = [[]];
1728
+ let truncated = false;
1729
+ for (const node of nodes) {
1730
+ const expanded = expandNode(node, text);
1731
+ truncated ||= expanded.truncated;
1732
+ const combined = combineVariants(variants, expanded.variants);
1733
+ variants = combined.variants;
1734
+ truncated ||= combined.truncated;
1735
+ }
1736
+ return { variants, truncated };
1737
+ }
1738
+ function expandNode(node, text) {
1739
+ if (node.kind === "literal" || node.kind === "value") return { variants: [[node]], truncated: false };
1740
+ if (node.kind === "conditional") {
1741
+ const variants2 = [];
1742
+ let truncated2 = false;
1743
+ for (const branch of node.branches) {
1744
+ const expanded = expandNodes(branch, text);
1745
+ truncated2 ||= expanded.truncated;
1746
+ for (const variant of expanded.variants) {
1747
+ if (variants2.length >= MAX_VARIANTS) {
1748
+ truncated2 = true;
1749
+ break;
1750
+ }
1751
+ variants2.push(variant);
1752
+ }
1753
+ }
1754
+ return { variants: variants2, truncated: truncated2 };
1755
+ }
1756
+ const body = expandNodes(node.body, text);
1757
+ let truncated = body.truncated;
1758
+ const minimumLiteralLength = Math.min(...body.variants.map(literalLength));
1759
+ const unboundedEmptyBody = minimumLiteralLength === 0;
1760
+ const maximumRepeats = unboundedEmptyBody ? 2 : Math.floor(text.length / minimumLiteralLength);
1761
+ if (unboundedEmptyBody) truncated = true;
1762
+ const variants = [[]];
1763
+ let repeated = [[]];
1764
+ for (let count = 1; count <= maximumRepeats; count += 1) {
1765
+ const combined = combineVariants(repeated, body.variants);
1766
+ repeated = combined.variants;
1767
+ truncated ||= combined.truncated;
1768
+ for (const variant of repeated) {
1769
+ if (variants.length >= MAX_VARIANTS) {
1770
+ truncated = true;
1771
+ return { variants, truncated };
1772
+ }
1773
+ variants.push(variant);
1774
+ }
1775
+ if (!repeated.length) break;
1776
+ }
1777
+ return { variants, truncated };
1778
+ }
1779
+ function combineVariants(left, right) {
1780
+ const variants = [];
1781
+ let truncated = false;
1782
+ for (const prefix of left) {
1783
+ for (const suffix of right) {
1784
+ if (variants.length >= MAX_VARIANTS) {
1785
+ truncated = true;
1786
+ return { variants, truncated };
1787
+ }
1788
+ variants.push([...prefix, ...suffix]);
1789
+ }
1790
+ }
1791
+ return { variants, truncated };
1792
+ }
1793
+ function literalLength(parts) {
1794
+ return parts.reduce((length, part) => length + (part.kind === "literal" ? part.value.length : 0), 0);
1795
+ }
1796
+ function linearize(parts) {
1797
+ const literals = [""];
1798
+ const expressions = [];
1799
+ for (const part of parts) {
1800
+ if (part.kind === "literal") literals[literals.length - 1] += part.value;
1801
+ else {
1802
+ expressions.push(part.expressionIndex);
1803
+ literals.push("");
1804
+ }
1805
+ }
1806
+ return { literals, expressions };
1807
+ }
1808
+ function enumerateLiteralPlacements(literals, text) {
1809
+ const placements = [];
1810
+ const placement = [];
1811
+ let truncated = false;
1812
+ const visit = (index, minimum) => {
1813
+ if (truncated) return;
1814
+ const literal = literals[index];
1815
+ if (index === literals.length - 1 && literal === "") {
1816
+ placement[index] = { start: text.length, end: text.length };
1817
+ if (placements.length >= MAX_LITERAL_PLACEMENTS) truncated = true;
1818
+ else placements.push(placement.slice());
1819
+ return;
1820
+ }
1821
+ if (literal === "") {
1822
+ const offset = index === 0 ? 0 : minimum;
1823
+ placement[index] = { start: offset, end: offset };
1824
+ visit(index + 1, offset);
1825
+ return;
1826
+ }
1827
+ let found = text.indexOf(literal, minimum);
1828
+ while (found >= 0 && !truncated) {
1829
+ placement[index] = { start: found, end: found + literal.length };
1830
+ if (index === literals.length - 1) {
1831
+ if (placements.length >= MAX_LITERAL_PLACEMENTS) truncated = true;
1832
+ else placements.push(placement.slice());
1833
+ } else {
1834
+ visit(index + 1, found + literal.length);
1835
+ }
1836
+ found = text.indexOf(literal, found + Math.max(1, literal.length));
1837
+ }
1838
+ };
1839
+ visit(0, 0);
1840
+ return { placements, truncated };
1841
+ }
1842
+ function tokenizeTemplate(template) {
1843
+ const lexemes = [];
1844
+ let literal = "";
1845
+ let position = 0;
1846
+ let expressionIndex = 0;
1847
+ const flushLiteral = () => {
1848
+ if (!literal) return;
1849
+ lexemes.push({ kind: "literal", value: literal });
1850
+ literal = "";
1851
+ };
1852
+ while (position < template.length) {
1853
+ if (template.startsWith("{{", position)) {
1854
+ literal += "{";
1855
+ position += 2;
1856
+ continue;
1857
+ }
1858
+ if (template.startsWith("}}", position)) {
1859
+ literal += "}";
1860
+ position += 2;
1861
+ continue;
1862
+ }
1863
+ if (template[position] !== "{") {
1864
+ literal += template[position];
1865
+ position += 1;
1866
+ continue;
1867
+ }
1868
+ const end = findTagEnd(template, position);
1869
+ if (end < 0) {
1870
+ literal += template.slice(position);
1871
+ break;
1872
+ }
1873
+ flushLiteral();
1874
+ const content = template.slice(position + 1, end).trim();
1875
+ const control = parseControl(content);
1876
+ lexemes.push(control ? { kind: "control", control } : { kind: "value", expressionIndex });
1877
+ expressionIndex += 1;
1878
+ position = end + 1;
1879
+ }
1880
+ flushLiteral();
1881
+ return lexemes;
1882
+ }
1883
+ function findTagEnd(value, start) {
1884
+ let depth = 1;
1885
+ let quote = "";
1886
+ for (let position = start + 1; position < value.length; position += 1) {
1887
+ const character = value[position];
1888
+ if (quote) {
1889
+ if (character === "\\") position += 1;
1890
+ else if (character === quote) quote = "";
1891
+ continue;
1892
+ }
1893
+ if (character === '"' || character === "'") quote = character;
1894
+ else if (character === "{") depth += 1;
1895
+ else if (character === "}" && --depth === 0) return position;
1896
+ }
1897
+ return -1;
1898
+ }
1899
+ function parseControl(content) {
1900
+ if (/^if(?:\s|$)/.test(content)) return "if";
1901
+ if (/^elseif(?:\s|$)/.test(content)) return "elseif";
1902
+ if (/^else\s*$/.test(content)) return "else";
1903
+ if (/^for(?:\s|$)/.test(content)) return "for";
1904
+ if (/^while(?:\s|$)/.test(content)) return "while";
1905
+ if (/^repeat(?:\s|$)/.test(content)) return "repeat";
1906
+ const close = content.match(/^\/(if|for|while|repeat)\s*$/)?.[1];
1907
+ return close ? `/${close}` : void 0;
1908
+ }
1909
+ function sameExpression(left, right) {
1910
+ return samePath(left.path, right.path) && left.range.start === right.range.start && left.range.end === right.range.end && left.content === right.content && left.occurrence === right.occurrence;
1911
+ }
1912
+ function samePath(left, right) {
1913
+ return left.length === right.length && left.every((part, index) => part === right[index]);
1914
+ }
1915
+ function deduplicateMatches(matches) {
1916
+ return matches.filter((match, index) => matches.findIndex((candidate) => candidate.evidenceId === match.evidenceId && candidate.range.start === match.range.start && candidate.range.end === match.range.end) === index);
1917
+ }
1918
+ function deduplicateRanges(ranges) {
1919
+ return ranges.filter((range, index) => ranges.findIndex((candidate) => candidate.start === range.start && candidate.end === range.end) === index);
1920
+ }
1921
+
1922
+ // src/presets/source-document.ts
1923
+ var import_yaml = require("yaml");
1924
+ var CONTROL_TAG = /^(?:if(?:\s|$)|elseif(?:\s|$)|else\s*$|for(?:\s|$)|while(?:\s|$)|repeat(?:\s|$)|\/(?:if|for|while|repeat)\s*$)/;
1925
+ function parsePresetSourceDocument(kind, source2) {
1926
+ const yaml = (0, import_yaml.parseDocument)(source2, { keepSourceTokens: true, prettyErrors: false });
1927
+ const diagnostics = yaml.errors.map((error) => ({
1928
+ code: "yaml-parse-error",
1929
+ message: error.message,
1930
+ severity: "error",
1931
+ range: error.pos?.length === 2 ? { start: error.pos[0], end: error.pos[1] } : void 0
1932
+ }));
1933
+ const locatedFields = [];
1934
+ if (kind === "core") {
1935
+ const prompts = yaml.get("prompts", true);
1936
+ if ((0, import_yaml.isSeq)(prompts)) {
1937
+ prompts.items.forEach((prompt, index) => {
1938
+ if (!(0, import_yaml.isMap)(prompt)) return;
1939
+ collectTemplateField(prompt.get("content", true), ["prompts", index, "content"], locatedFields, diagnostics);
1940
+ });
1941
+ }
1942
+ collectTemplateField(yaml.get("format_user_prompt", true), ["format_user_prompt"], locatedFields, diagnostics);
1943
+ } else {
1944
+ collectTemplateField(yaml.get("system", true), ["system"], locatedFields, diagnostics);
1945
+ collectTemplateField(yaml.get("input", true), ["input"], locatedFields, diagnostics);
1946
+ }
1947
+ const expressions = locatedFields.flatMap((field) => scanTemplateExpressions(field, source2));
1948
+ const templateFields = locatedFields.map(({ path, range, value }) => ({ path, range, value }));
1949
+ const displayName = diagnostics.some(({ code }) => code === "yaml-parse-error") ? void 0 : readDisplayName(kind, yaml);
1950
+ return { kind, source: source2, displayName, templateFields, expressions, diagnostics };
1951
+ }
1952
+ function readDisplayName(kind, yaml) {
1953
+ if (kind === "character") return trimmedScalarString(yaml.get("name", true));
1954
+ const keywords = yaml.get("keywords", true);
1955
+ return (0, import_yaml.isSeq)(keywords) ? trimmedScalarString(keywords.items[0]) : void 0;
1956
+ }
1957
+ function trimmedScalarString(node) {
1958
+ if (!(0, import_yaml.isScalar)(node) || typeof node.value !== "string" || !node.value.trim()) return void 0;
1959
+ return node.value;
1960
+ }
1961
+ function collectTemplateField(node, path, fields, diagnostics) {
1962
+ if (node == null) return;
1963
+ if (!(0, import_yaml.isScalar)(node) || typeof node.value !== "string") {
1964
+ const range = nodeRange(node);
1965
+ diagnostics.push({
1966
+ code: "template-field-not-string",
1967
+ message: `\u6A21\u677F\u5B57\u6BB5 ${formatPath(path)} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`,
1968
+ severity: "error",
1969
+ path,
1970
+ range
1971
+ });
1972
+ return;
1973
+ }
1974
+ const location = locateScalar(node);
1975
+ if (!location) {
1976
+ diagnostics.push({
1977
+ code: "template-field-not-string",
1978
+ message: `\u65E0\u6CD5\u5B9A\u4F4D\u6A21\u677F\u5B57\u6BB5 ${formatPath(path)} \u7684\u6E90\u7801\u8303\u56F4`,
1979
+ severity: "error",
1980
+ path,
1981
+ range: nodeRange(node)
1982
+ });
1983
+ return;
1984
+ }
1985
+ fields.push({ path, range: location.range, value: node.value, location });
1986
+ }
1987
+ function scanTemplateExpressions(field, source2) {
1988
+ const expressions = [];
1989
+ let position = 0;
1990
+ let occurrence = 0;
1991
+ while (position < field.value.length) {
1992
+ if (field.value.startsWith("{{", position) || field.value.startsWith("}}", position)) {
1993
+ position += 2;
1994
+ continue;
1995
+ }
1996
+ if (field.value[position] !== "{") {
1997
+ position += 1;
1998
+ continue;
1999
+ }
2000
+ const end = findTemplateTagEnd(field.value, position);
2001
+ if (end < 0) break;
2002
+ const content = field.value.slice(position + 1, end);
2003
+ const startOffset = field.location.offsets[position];
2004
+ const endOffset = field.location.ends[end];
2005
+ if (startOffset != null && endOffset != null && source2[startOffset] === "{" && source2[endOffset - 1] === "}") {
2006
+ const range = { start: startOffset, end: endOffset };
2007
+ const sourceText = source2.slice(range.start, range.end);
2008
+ if (sourceText.startsWith("{") && sourceText.endsWith("}")) {
2009
+ const kind = CONTROL_TAG.test(content.trim()) ? "control" : "value";
2010
+ expressions.push({
2011
+ path: field.path,
2012
+ range,
2013
+ content,
2014
+ kind,
2015
+ clickable: kind === "value",
2016
+ occurrence
2017
+ });
2018
+ occurrence += 1;
2019
+ }
2020
+ }
2021
+ position = end + 1;
2022
+ }
2023
+ return expressions;
2024
+ }
2025
+ function findTemplateTagEnd(value, start) {
2026
+ let depth = 1;
2027
+ let quote = "";
2028
+ for (let position = start + 1; position < value.length; position += 1) {
2029
+ const character = value[position];
2030
+ if (quote) {
2031
+ if (character === "\\") position += 1;
2032
+ else if (character === quote) quote = "";
2033
+ continue;
2034
+ }
2035
+ if (character === '"' || character === "'") {
2036
+ quote = character;
2037
+ } else if (character === "{") {
2038
+ depth += 1;
2039
+ } else if (character === "}") {
2040
+ depth -= 1;
2041
+ if (depth === 0) return position;
2042
+ }
2043
+ }
2044
+ return -1;
2045
+ }
2046
+ function locateScalar(node) {
2047
+ const range = node.range;
2048
+ const token = node.srcToken;
2049
+ if (!range || !token || typeof node.value !== "string") return void 0;
2050
+ let mapped;
2051
+ switch (token.type) {
2052
+ case "scalar":
2053
+ mapped = locateFlowFolded(token.source, token.offset);
2054
+ break;
2055
+ case "single-quoted-scalar":
2056
+ mapped = collapseSingleQuotedEscapes(locateFlowFolded(token.source.slice(1, -1), token.offset + 1));
2057
+ break;
2058
+ case "double-quoted-scalar":
2059
+ mapped = locateDoubleQuoted(token.source, token.offset);
2060
+ break;
2061
+ case "block-scalar":
2062
+ mapped = locateBlockScalar(token);
2063
+ break;
2064
+ default:
2065
+ return void 0;
2066
+ }
2067
+ if (mapped.value !== node.value) return void 0;
2068
+ return {
2069
+ range: { start: range[0], end: range[1] },
2070
+ value: mapped.value,
2071
+ offsets: mapped.offsets,
2072
+ ends: mapped.ends
2073
+ };
2074
+ }
2075
+ function locateFlowFolded(source2, sourceStart) {
2076
+ let first;
2077
+ let line;
2078
+ try {
2079
+ first = new RegExp("(.*?)(?<![ \\t])[ \\t]*\\r?\\n", "sy");
2080
+ line = new RegExp("[ \\t]*(.*?)(?:(?<![ \\t])[ \\t]*)?\\r?\\n", "sy");
2081
+ } catch {
2082
+ first = /(.*?)[ \t]*\r?\n/sy;
2083
+ line = /[ \t]*(.*?)[ \t]*\r?\n/sy;
2084
+ }
2085
+ const builder = mappedBuilder();
2086
+ let match = first.exec(source2);
2087
+ if (!match) {
2088
+ builder.appendSource(source2, sourceStart);
2089
+ return builder.result();
2090
+ }
2091
+ builder.appendSource(match[1], sourceStart);
2092
+ let separator = " ";
2093
+ let separatorStart = match[0].length - newlineWidth(match[0]);
2094
+ let position = first.lastIndex;
2095
+ line.lastIndex = position;
2096
+ while (match = line.exec(source2)) {
2097
+ const leadingWhitespace2 = match[0].length - match[0].trimStart().length;
2098
+ const contentStart = position + leadingWhitespace2;
2099
+ const newlineStart = position + match[0].length - newlineWidth(match[0]);
2100
+ if (match[1] === "") {
2101
+ if (separator === "\n") builder.appendGenerated(separator, sourceStart + separatorStart, sourceStart + separatorStart + newlineWidthAt(source2, separatorStart));
2102
+ else separator = "\n";
2103
+ } else {
2104
+ builder.appendGenerated(separator, sourceStart + separatorStart, sourceStart + separatorStart + newlineWidthAt(source2, separatorStart));
2105
+ builder.appendSource(match[1], sourceStart + contentStart);
2106
+ separator = " ";
2107
+ }
2108
+ separatorStart = newlineStart;
2109
+ position = line.lastIndex;
2110
+ }
2111
+ const last = /[ \t]*(.*)/sy;
2112
+ last.lastIndex = position;
2113
+ match = last.exec(source2);
2114
+ const lastText = match?.[1] ?? "";
2115
+ const leadingWhitespace = (match?.[0].length ?? 0) - lastText.length;
2116
+ builder.appendGenerated(separator, sourceStart + separatorStart, sourceStart + separatorStart + newlineWidthAt(source2, separatorStart));
2117
+ builder.appendSource(lastText, sourceStart + position + leadingWhitespace);
2118
+ return builder.result();
2119
+ }
2120
+ function collapseSingleQuotedEscapes(mapped) {
2121
+ const builder = mappedBuilder();
2122
+ for (let position = 0; position < mapped.value.length; position += 1) {
2123
+ if (mapped.value[position] === "'" && mapped.value[position + 1] === "'") {
2124
+ builder.appendMapped("'", mapped.offsets[position], mapped.ends[position + 1]);
2125
+ position += 1;
2126
+ } else {
2127
+ builder.appendMapped(mapped.value[position], mapped.offsets[position], mapped.ends[position]);
2128
+ }
2129
+ }
2130
+ return builder.result();
2131
+ }
2132
+ function locateDoubleQuoted(source2, sourceStart) {
2133
+ const builder = mappedBuilder();
2134
+ for (let position = 1; position < source2.length - 1; position += 1) {
2135
+ const character = source2[position];
2136
+ if (character === "\r" && source2[position + 1] === "\n") continue;
2137
+ if (character === "\n") {
2138
+ const folded = foldDoubleQuotedNewline(source2, position);
2139
+ builder.appendGenerated(folded.value, sourceStart + position, sourceStart + folded.offset + 1);
2140
+ position = folded.offset;
2141
+ continue;
2142
+ }
2143
+ if (character === "\\") {
2144
+ let next = source2[++position];
2145
+ const escapeStart = position - 1;
2146
+ const simple = DOUBLE_QUOTED_ESCAPES[next];
2147
+ if (simple != null) {
2148
+ builder.appendGenerated(simple, sourceStart + escapeStart, sourceStart + position + 1);
2149
+ } else if (next === "\n") {
2150
+ next = source2[position + 1];
2151
+ while (next === " " || next === " ") next = source2[++position + 1];
2152
+ } else if (next === "\r" && source2[position + 1] === "\n") {
2153
+ position += 1;
2154
+ next = source2[position + 1];
2155
+ while (next === " " || next === " ") next = source2[++position + 1];
2156
+ } else if (next === "x" || next === "u" || next === "U") {
2157
+ const width = next === "x" ? 2 : next === "u" ? 4 : 8;
2158
+ const codePoint = Number.parseInt(source2.slice(position + 1, position + 1 + width), 16);
2159
+ const decoded = String.fromCodePoint(codePoint);
2160
+ builder.appendGenerated(decoded, sourceStart + escapeStart, sourceStart + position + width + 1);
2161
+ position += width;
2162
+ } else {
2163
+ builder.appendSource(source2.slice(escapeStart, position + 1), sourceStart + escapeStart);
2164
+ }
2165
+ continue;
2166
+ }
2167
+ if (character === " " || character === " ") {
2168
+ const whitespaceStart = position;
2169
+ let next = source2[position + 1];
2170
+ while (next === " " || next === " ") next = source2[++position + 1];
2171
+ if (next !== "\n" && !(next === "\r" && source2[position + 2] === "\n")) {
2172
+ builder.appendSource(source2.slice(whitespaceStart, position + 1), sourceStart + whitespaceStart);
2173
+ }
2174
+ continue;
2175
+ }
2176
+ builder.appendSource(character, sourceStart + position);
2177
+ }
2178
+ return builder.result();
2179
+ }
2180
+ function foldDoubleQuotedNewline(source2, start) {
2181
+ let value = "";
2182
+ let offset = start;
2183
+ let character = source2[offset + 1];
2184
+ while (character === " " || character === " " || character === "\n" || character === "\r") {
2185
+ if (character === "\r" && source2[offset + 2] !== "\n") break;
2186
+ if (character === "\n") value += "\n";
2187
+ offset += 1;
2188
+ character = source2[offset + 1];
2189
+ }
2190
+ return { value: value || " ", offset };
2191
+ }
2192
+ var DOUBLE_QUOTED_ESCAPES = {
2193
+ "0": "\0",
2194
+ a: "\x07",
2195
+ b: "\b",
2196
+ e: "\x1B",
2197
+ f: "\f",
2198
+ n: "\n",
2199
+ r: "\r",
2200
+ t: " ",
2201
+ v: "\v",
2202
+ N: "\x85",
2203
+ _: "\xA0",
2204
+ L: "\u2028",
2205
+ P: "\u2029",
2206
+ " ": " ",
2207
+ '"': '"',
2208
+ "/": "/",
2209
+ "\\": "\\",
2210
+ " ": " "
2211
+ };
2212
+ function locateBlockScalar(token) {
2213
+ const header = parseBlockHeader(token);
2214
+ const contentBase = token.offset + header.length;
2215
+ const lines = splitBlockLines(token.source);
2216
+ let chompStart = lines.length;
2217
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
2218
+ const content = lines[index].content;
2219
+ if (content === "" || content === "\r") chompStart = index;
2220
+ else break;
2221
+ }
2222
+ const builder = mappedBuilder();
2223
+ if (chompStart === 0) {
2224
+ if (header.chomp === "+" && lines.length > 0) {
2225
+ builder.appendGenerated("\n".repeat(Math.max(1, lines.length - 1)), contentBase, contentBase + token.source.length);
2226
+ }
2227
+ return builder.result();
2228
+ }
2229
+ let trimIndent = token.indent + header.indent;
2230
+ let contentStart = 0;
2231
+ for (let index = 0; index < chompStart; index += 1) {
2232
+ const line = lines[index];
2233
+ if (line.content === "" || line.content === "\r") {
2234
+ if (header.indent === 0 && line.indent.length > trimIndent) trimIndent = line.indent.length;
2235
+ } else {
2236
+ if (header.indent === 0) trimIndent = line.indent.length;
2237
+ contentStart = index;
2238
+ break;
2239
+ }
2240
+ }
2241
+ for (let index = lines.length - 1; index >= chompStart; index -= 1) {
2242
+ if (lines[index].indent.length > trimIndent) chompStart = index + 1;
2243
+ }
2244
+ let separator = "";
2245
+ let separatorRange = { start: contentBase, end: contentBase };
2246
+ let previousMoreIndented = false;
2247
+ for (let index = 0; index < contentStart; index += 1) {
2248
+ const line = lines[index];
2249
+ builder.appendSource(line.indent.slice(trimIndent), contentBase + line.contentOffset - line.indent.length + trimIndent);
2250
+ builder.appendGenerated("\n", contentBase + line.newlineStart, contentBase + line.newlineEnd);
2251
+ }
2252
+ for (let index = contentStart; index < chompStart; index += 1) {
2253
+ const line = lines[index];
2254
+ let content = line.content;
2255
+ if (content.endsWith("\r")) content = content.slice(0, -1);
2256
+ const retainedIndent = line.indent.slice(trimIndent);
2257
+ const retainedOffset = contentBase + line.contentOffset - line.indent.length + trimIndent;
2258
+ if (header.mode === "|") {
2259
+ builder.appendGenerated(separator, separatorRange.start, separatorRange.end);
2260
+ builder.appendSource(retainedIndent + content, retainedOffset);
2261
+ separator = "\n";
2262
+ } else if (line.indent.length > trimIndent || content[0] === " ") {
2263
+ if (separator === " ") separator = "\n";
2264
+ else if (!previousMoreIndented && separator === "\n") separator = "\n\n";
2265
+ builder.appendGenerated(separator, separatorRange.start, separatorRange.end);
2266
+ builder.appendSource(retainedIndent + content, retainedOffset);
2267
+ separator = "\n";
2268
+ previousMoreIndented = true;
2269
+ } else if (content === "") {
2270
+ if (separator === "\n") builder.appendGenerated("\n", separatorRange.start, separatorRange.end);
2271
+ else separator = "\n";
2272
+ } else {
2273
+ builder.appendGenerated(separator, separatorRange.start, separatorRange.end);
2274
+ builder.appendSource(content, contentBase + line.contentOffset);
2275
+ separator = " ";
2276
+ previousMoreIndented = false;
2277
+ }
2278
+ separatorRange = { start: contentBase + line.newlineStart, end: contentBase + line.newlineEnd };
2279
+ }
2280
+ if (header.chomp === "+") {
2281
+ for (let index = chompStart; index < lines.length; index += 1) {
2282
+ const line = lines[index];
2283
+ builder.appendGenerated("\n", separatorRange.start, separatorRange.end);
2284
+ builder.appendSource(line.indent.slice(trimIndent), contentBase + line.contentOffset - line.indent.length + trimIndent);
2285
+ separatorRange = { start: contentBase + line.newlineStart, end: contentBase + line.newlineEnd };
2286
+ }
2287
+ if (!builder.value().endsWith("\n")) builder.appendGenerated("\n", separatorRange.start, separatorRange.end);
2288
+ } else if (header.chomp !== "-") {
2289
+ builder.appendGenerated("\n", separatorRange.start, separatorRange.end);
2290
+ }
2291
+ return builder.result();
2292
+ }
2293
+ function parseBlockHeader(token) {
2294
+ const props = token.props ?? [];
2295
+ const headerSource = props[0]?.source ?? "|";
2296
+ let indent = 0;
2297
+ let chomp = "";
2298
+ for (let index = 1; index < headerSource.length; index += 1) {
2299
+ const character = headerSource[index];
2300
+ if (!chomp && (character === "-" || character === "+")) chomp = character;
2301
+ else if (!indent && Number(character)) indent = Number(character);
2302
+ }
2303
+ return {
2304
+ mode: headerSource[0] === ">" ? ">" : "|",
2305
+ indent,
2306
+ chomp,
2307
+ length: props.reduce((length, prop) => length + prop.source.length, 0)
2308
+ };
2309
+ }
2310
+ function splitBlockLines(source2) {
2311
+ const lines = [];
2312
+ let offset = 0;
2313
+ while (offset <= source2.length) {
2314
+ const newline = source2.indexOf("\n", offset);
2315
+ const lineEnd = newline < 0 ? source2.length : newline;
2316
+ const raw = source2.slice(offset, lineEnd);
2317
+ const indent = raw.match(/^ */)?.[0] ?? "";
2318
+ lines.push({
2319
+ indent,
2320
+ content: raw.slice(indent.length),
2321
+ contentOffset: offset + indent.length,
2322
+ newlineStart: lineEnd > offset && source2[lineEnd - 1] === "\r" ? lineEnd - 1 : lineEnd,
2323
+ newlineEnd: newline < 0 ? lineEnd : newline + 1
2324
+ });
2325
+ if (newline < 0) break;
2326
+ offset = newline + 1;
2327
+ }
2328
+ return lines;
2329
+ }
2330
+ function mappedBuilder() {
2331
+ let value = "";
2332
+ const offsets = [];
2333
+ const ends = [];
2334
+ return {
2335
+ appendSource(text, start) {
2336
+ value += text;
2337
+ for (let index = 0; index < text.length; index += 1) {
2338
+ offsets.push(start + index);
2339
+ ends.push(start + index + 1);
2340
+ }
2341
+ },
2342
+ appendGenerated(text, start, end) {
2343
+ value += text;
2344
+ for (let index = 0; index < text.length; index += 1) {
2345
+ offsets.push(start);
2346
+ ends.push(end);
2347
+ }
2348
+ },
2349
+ appendMapped(text, start, end) {
2350
+ value += text;
2351
+ offsets.push(start);
2352
+ ends.push(end);
2353
+ },
2354
+ value: () => value,
2355
+ result: () => ({ value, offsets, ends })
2356
+ };
2357
+ }
2358
+ function newlineWidth(text) {
2359
+ return text.endsWith("\r\n") ? 2 : 1;
2360
+ }
2361
+ function newlineWidthAt(source2, position) {
2362
+ return source2[position] === "\r" && source2[position + 1] === "\n" ? 2 : 1;
2363
+ }
2364
+ function nodeRange(node) {
2365
+ if (!node || typeof node !== "object" || !("range" in node)) return void 0;
2366
+ const range = node.range;
2367
+ return range ? { start: range[0], end: range[1] } : void 0;
2368
+ }
2369
+ function formatPath(path) {
2370
+ return path.map((part) => typeof part === "number" ? `[${part}]` : part).join(".");
2371
+ }
2372
+
2373
+ // src/model-request-variables.ts
2374
+ function deriveModelRequestVariables(snapshots, evidence) {
2375
+ const messages = new Map(evidence.requestMessages.map((message) => [message.evidenceId, message]));
2376
+ return snapshots.flatMap((snapshot, snapshotIndex) => {
2377
+ const document = documentFromRuntimeSnapshot(snapshot);
2378
+ return document.expressions.filter((expression) => expression.kind === "value").map((expression) => {
2379
+ const matched = matchPresetExpressionEvidence({ document, expression, snapshot, evidence });
2380
+ const base = {
2381
+ id: `${snapshot.kind}:${snapshotIndex}:${JSON.stringify(expression.path)}#${expression.occurrence}`,
2382
+ name: expression.content.trim(),
2383
+ presetKind: snapshot.kind,
2384
+ presetName: snapshot.presetName,
2385
+ path: expression.path,
2386
+ occurrence: expression.occurrence
2387
+ };
2388
+ if (matched.status !== "matched") return { ...base, status: matched.status };
2389
+ const message = messages.get(matched.evidenceId);
2390
+ if (!message || matched.range.start < 0 || matched.range.end < matched.range.start || matched.range.end > message.text.length) {
2391
+ return { ...base, status: "not-observed" };
2392
+ }
2393
+ return {
2394
+ ...base,
2395
+ status: "observed",
2396
+ value: message.text.slice(matched.range.start, matched.range.end),
2397
+ evidenceId: matched.evidenceId,
2398
+ range: matched.range
2399
+ };
2400
+ });
2401
+ });
2402
+ }
2403
+ function modelRequestVariableStatusLabel(status) {
2404
+ if (status === "ambiguous") return "\u5C55\u5F00\u503C\u5B58\u5728\u6B67\u4E49";
2405
+ if (status === "stale") return "\u9884\u8BBE\u5FEB\u7167\u5DF2\u53D8\u5316";
2406
+ if (status === "unsupported") return "\u8868\u8FBE\u5F0F\u4E0D\u652F\u6301\u5B9A\u4F4D";
2407
+ return "\u672A\u5728\u6A21\u578B\u8BF7\u6C42\u4E2D\u89C2\u5BDF\u5230\u5C55\u5F00\u503C";
2408
+ }
2409
+ function documentFromRuntimeSnapshot(snapshot) {
2410
+ if (snapshot.kind === "character") {
2411
+ const fields = Object.fromEntries(snapshot.templates.flatMap((template) => template.path[0] === "system" || template.path[0] === "input" ? [[template.path[0], template.template]] : []));
2412
+ return parsePresetSourceDocument("character", (0, import_yaml2.stringify)(fields));
2413
+ }
2414
+ const prompts = [];
2415
+ let formatUserPrompt;
2416
+ for (const template of snapshot.templates) {
2417
+ if (template.path[0] === "format_user_prompt") {
2418
+ formatUserPrompt = template.template;
2419
+ continue;
2420
+ }
2421
+ const index = template.path[0] === "prompts" && typeof template.path[1] === "number" ? template.path[1] : void 0;
2422
+ if (index !== void 0) prompts[index] = { role: template.role, content: template.template };
2423
+ }
2424
+ return parsePresetSourceDocument("core", (0, import_yaml2.stringify)({
2425
+ prompts: prompts.map((prompt) => prompt ?? {}),
2426
+ ...formatUserPrompt !== void 0 ? { format_user_prompt: formatUserPrompt } : {}
2427
+ }));
2428
+ }
2429
+
2430
+ // src/record-store.ts
2431
+ var ScopeRowIndex = class {
2432
+ rows = [];
2433
+ nextSequence = 1;
2434
+ reset(rows, persistedNextSequence) {
2435
+ this.rows = rows.map(({ sequence, bytes }) => ({ sequence: Number(sequence), bytes: Number(bytes) || 0 })).sort((left, right) => left.sequence - right.sequence);
2436
+ this.nextSequence = Math.max(
2437
+ 1,
2438
+ persistedNextSequence,
2439
+ this.rows.reduce((max, row) => Math.max(max, row.sequence + 1), 1)
2440
+ );
2441
+ }
2442
+ put(sequence, bytes, nextSequence = 0) {
2443
+ const index = this.rows.findIndex((row) => row.sequence === sequence);
2444
+ if (index >= 0) this.rows[index] = { sequence, bytes };
2445
+ else {
2446
+ this.rows.push({ sequence, bytes });
2447
+ this.rows.sort((left, right) => left.sequence - right.sequence);
2448
+ }
2449
+ this.nextSequence = Math.max(this.nextSequence, nextSequence, sequence + 1);
2450
+ }
2451
+ has(sequence) {
2452
+ return this.rows.some((row) => row.sequence === sequence);
2453
+ }
2454
+ clear(nextSequence) {
2455
+ this.rows = [];
2456
+ this.nextSequence = Math.max(this.nextSequence, nextSequence);
2457
+ }
2458
+ /** 从最旧开始试算,返回需要一并删除的最大序号;索引在删除成功后才由 dropThrough 收敛。 */
2459
+ resolveCutoff(limits) {
2460
+ let count = this.rows.length;
2461
+ let totalBytes = this.totalBytes();
2462
+ let cutoff;
2463
+ for (const row of this.rows) {
2464
+ if (count <= limits.maxRecords && totalBytes <= limits.maxBytes) break;
2465
+ cutoff = row.sequence;
2466
+ totalBytes -= row.bytes;
2467
+ count -= 1;
2468
+ }
2469
+ return cutoff;
2470
+ }
2471
+ dropThrough(cutoff) {
2472
+ this.rows = this.rows.filter((row) => row.sequence > cutoff);
2473
+ }
2474
+ readNextSequence() {
2475
+ return this.nextSequence;
2476
+ }
2477
+ summary() {
2478
+ return {
2479
+ nextSequence: this.nextSequence,
2480
+ recordCount: this.rows.length,
2481
+ totalBytes: this.totalBytes(),
2482
+ ...this.rows.length ? { earliestSequence: this.rows[0].sequence } : {}
2483
+ };
2484
+ }
2485
+ totalBytes() {
2486
+ return this.rows.reduce((sum, row) => sum + row.bytes, 0);
2487
+ }
2488
+ };
2489
+ var InMemoryRecordRows = class {
2490
+ constructor(matches) {
2491
+ this.matches = matches;
2492
+ }
2493
+ matches;
2494
+ bodies = /* @__PURE__ */ new Map();
2495
+ index = new ScopeRowIndex();
2496
+ async summarize() {
2497
+ return this.index.summary();
2498
+ }
2499
+ async append(record, bytes, nextSequence) {
2500
+ this.bodies.set(record.sequence, structuredClone(record));
2501
+ this.index.put(record.sequence, bytes, nextSequence);
2502
+ return this.index.summary();
2503
+ }
2504
+ async find(recordId) {
2505
+ for (const record of this.bodies.values()) {
2506
+ if (record.id === recordId) return structuredClone(record);
2507
+ }
2508
+ }
2509
+ async replace(record, bytes) {
2510
+ if (this.index.has(record.sequence)) {
2511
+ this.bodies.set(record.sequence, structuredClone(record));
2512
+ this.index.put(record.sequence, bytes);
2513
+ }
2514
+ return this.index.summary();
2515
+ }
2516
+ async query(query) {
2517
+ return this.select(query).map((record) => structuredClone(record));
2518
+ }
2519
+ /**
2520
+ * 只读出记录的一部分。投影先在存有的行上执行,再拷贝投影结果,因此读取路径不需要的子树
2521
+ * (模型请求的请求体与响应原文)既不会被深拷贝,也不会被交给调用方。
2522
+ */
2523
+ async queryProjected(query, project) {
2524
+ return this.select(query).map((record) => structuredClone(project(record)));
2525
+ }
2526
+ select(query) {
2527
+ return [...this.bodies.values()].filter((record) => this.matches(record, query)).sort((left, right) => (query.order === "asc" ? 1 : -1) * (left.sequence - right.sequence)).slice(0, Math.max(0, query.limit));
2528
+ }
2529
+ async reclaim(limits) {
2530
+ const cutoff = this.index.resolveCutoff(limits);
2531
+ if (cutoff !== void 0) {
2532
+ for (const sequence of [...this.bodies.keys()]) {
2533
+ if (sequence <= cutoff) this.bodies.delete(sequence);
2534
+ }
2535
+ this.index.dropThrough(cutoff);
2536
+ }
2537
+ return this.index.summary();
2538
+ }
2539
+ async clear(nextSequence) {
2540
+ this.bodies.clear();
2541
+ this.index.clear(nextSequence);
2542
+ return this.index.summary();
2543
+ }
2544
+ };
2545
+ var SerialWriteQueue = class {
2546
+ constructor(ready) {
2547
+ this.ready = ready;
2548
+ }
2549
+ ready;
2550
+ queue = Promise.resolve();
2551
+ /** 队列任务可能在等待期间继续追加(例如 append 之后的容量回收),必须等到队列稳定。 */
2552
+ async settle() {
2553
+ await this.ready;
2554
+ for (let guard = 0; guard < 1e3; guard += 1) {
2555
+ const queue = this.queue;
2556
+ await queue;
2557
+ if (queue === this.queue) return;
2558
+ }
2559
+ }
2560
+ /** 写入失败不能变成未处理的 Promise 拒绝:证据落盘是旁路能力,不应影响调用方。 */
2561
+ push(task) {
2562
+ void this.run(task).catch(() => void 0);
2563
+ }
2564
+ run(task) {
2565
+ const result = this.queue.then(() => this.ready).then(task);
2566
+ this.queue = result.then(() => void 0, () => void 0);
2567
+ return result;
2568
+ }
2569
+ };
2570
+
2571
+ // src/types.ts
2572
+ var StudioDomainError = class extends Error {
2573
+ constructor(message) {
2574
+ super(message);
2575
+ this.name = "StudioDomainError";
2576
+ }
2577
+ };
2578
+ var StudioModelRequestCursorExpiredError = class extends StudioDomainError {
2579
+ constructor(message, earliestCursor) {
2580
+ super(message);
2581
+ this.earliestCursor = earliestCursor;
2582
+ this.name = "StudioModelRequestCursorExpiredError";
2583
+ }
2584
+ earliestCursor;
2585
+ code = "cursor_expired";
2586
+ };
2587
+
2588
+ // src/model-request.ts
2589
+ var DEFAULT_MODEL_REQUEST_RECORD_LIMIT = 500;
2590
+ var DEFAULT_MODEL_REQUEST_RECORD_MAX_BYTES = 50 * 1024 * 1024;
2591
+ var DEFAULT_MODEL_REQUEST_PAGE_SIZE = 50;
2592
+ var MAX_MODEL_REQUEST_PAGE_SIZE = 200;
2593
+ var MODEL_REQUEST_FACET_SCAN_LIMIT = 500;
2594
+ function toModelRequestRecordHeader(record) {
2595
+ const { requestBody: _requestBody, responseBodyRaw: _responseBodyRaw, ...header } = record;
2596
+ return header;
2597
+ }
2598
+ function matchesModelRequestQuery(record, query) {
2599
+ if (query.attribution && record.attribution !== query.attribution) return false;
2600
+ if (query.botId && record.entities.botId !== query.botId) return false;
2601
+ if (query.conversationId && record.entities.conversationId !== query.conversationId) return false;
2602
+ if (query.interactionId && record.interactionId !== query.interactionId) return false;
2603
+ if (query.model && record.model !== query.model) return false;
2604
+ if (query.errorsOnly && record.status !== "error") return false;
2605
+ if (query.beforeSequence !== void 0) {
2606
+ return query.order === "asc" ? record.sequence > query.beforeSequence : record.sequence < query.beforeSequence;
2607
+ }
2608
+ if (!query.beforeCreatedAt) return true;
2609
+ const created = record.createdAt.localeCompare(query.beforeCreatedAt);
2610
+ if (created !== 0) return query.order === "asc" ? created > 0 : created < 0;
2611
+ if (!query.beforeId) return true;
2612
+ const id = record.id.localeCompare(query.beforeId);
2613
+ return query.order === "asc" ? id > 0 : id < 0;
2614
+ }
2615
+ var InMemoryModelRequestRecords = class extends InMemoryRecordRows {
2616
+ constructor() {
2617
+ super(matchesModelRequestQuery);
2618
+ }
2619
+ queryHeaders(query) {
2620
+ return this.queryProjected(query, toModelRequestRecordHeader);
2621
+ }
2622
+ };
2623
+ function estimateModelRequestRecordBytes(record) {
2624
+ return Buffer.byteLength(JSON.stringify(record), "utf8");
2625
+ }
2626
+ function createModelRequestError(error, traceId = import_koishi2.Random.id()) {
2627
+ const message = error instanceof Error ? error.message : String(error ?? "\u6A21\u578B\u8BF7\u6C42\u5931\u8D25");
2628
+ const lower = message.toLowerCase();
2629
+ const retryable = lower.includes("timeout") || lower.includes("\u8D85\u65F6") || lower.includes("rate limit") || lower.includes("econnreset");
2630
+ return { code: retryable ? "transient_error" : "model_request_error", message, retryable, traceId };
2631
+ }
2632
+ function summarizePresetSnapshots(snapshots) {
2633
+ if (!snapshots?.length) return;
2634
+ return snapshots.map((snapshot) => ({
2635
+ kind: snapshot.kind,
2636
+ presetName: snapshot.presetName,
2637
+ capturedAt: snapshot.capturedAt,
2638
+ templateCount: snapshot.templates.length
2639
+ }));
2640
+ }
2641
+ function presentModelRequestListItem(record) {
2642
+ const {
2643
+ requestBody: _requestBody,
2644
+ responseBodyRaw: _responseBodyRaw,
2645
+ presetSnapshots,
2646
+ ...listItem
2647
+ } = record;
2648
+ const presetSnapshotSummaries = summarizePresetSnapshots(presetSnapshots);
2649
+ return {
2650
+ ...structuredClone(listItem),
2651
+ ...presetSnapshotSummaries ? { presetSnapshotSummaries } : {}
2652
+ };
2653
+ }
2654
+ function presentModelRequestDetail(record) {
2655
+ const evidence = record.requestBody === void 0 ? void 0 : projectModelEvidence({ requestBody: record.requestBody });
2656
+ return {
2657
+ ...structuredClone(record),
2658
+ // 字段数是原始 JSON 事实,只有请求体确实是对象时才存在;非对象请求体不暗示它有 0 个字段。
2659
+ ...isRequestBodyObject(record.requestBody) ? { requestBodyKeyCount: Object.keys(record.requestBody).length } : {},
2660
+ ...evidence ? {
2661
+ evidenceCounts: {
2662
+ requestMessageCount: evidence.requestMessages.length,
2663
+ toolDefinitionCount: evidence.toolDefinitions.length
2664
+ }
2665
+ } : {},
2666
+ variables: evidence && record.presetSnapshots?.length ? deriveModelRequestVariables(record.presetSnapshots, evidence) : []
2667
+ };
2668
+ }
2669
+ function isRequestBodyObject(body) {
2670
+ return Boolean(body && typeof body === "object" && !Array.isArray(body));
2671
+ }
2672
+ var StudioModelRequestStore = class {
2673
+ nextSequence = 1;
2674
+ summary = { nextSequence: 1, recordCount: 0, totalBytes: 0 };
2675
+ maxRecords;
2676
+ maxBytes;
2677
+ persistence;
2678
+ ready;
2679
+ writes;
2680
+ /** 已分配序号但尚未落盘的记录;恢复完成时按持久化高水位重新编号。 */
2681
+ staged = [];
2682
+ pendingUpdates = /* @__PURE__ */ new Set();
2683
+ constructor(options = {}) {
2684
+ this.maxRecords = options.maxRecords ?? DEFAULT_MODEL_REQUEST_RECORD_LIMIT;
2685
+ this.maxBytes = options.maxBytes ?? DEFAULT_MODEL_REQUEST_RECORD_MAX_BYTES;
2686
+ this.persistence = options.persistence ?? new InMemoryModelRequestRecords();
2687
+ this.ready = this.persistence.summarize().then((summary) => {
2688
+ let nextSequence = Math.max(1, summary.nextSequence);
2689
+ for (const record of this.staged) record.sequence = nextSequence++;
2690
+ this.nextSequence = nextSequence;
2691
+ this.summary = { ...summary, nextSequence };
2692
+ }).catch(() => {
2693
+ this.persistence = new InMemoryModelRequestRecords();
2694
+ });
2695
+ this.writes = new SerialWriteQueue(this.ready);
2696
+ }
2697
+ waitForReady() {
2698
+ return this.ready;
2699
+ }
2700
+ async waitForPersistence() {
2701
+ for (let guard = 0; guard < 1e3; guard += 1) {
2702
+ await this.settle();
2703
+ if (!this.pendingUpdates.size) return;
2704
+ await Promise.all([...this.pendingUpdates]);
2705
+ }
2706
+ }
2707
+ /**
2708
+ * 等到已入队的写入全部提交。读取只需要这一层:pendingUpdates 里是尚未产生写入的
2709
+ * 外部 Promise(例如仍在流式传输的响应体),读取等待它们会与写入方互相死锁。
2710
+ */
2711
+ settle() {
2712
+ return this.writes.settle();
2713
+ }
2714
+ /** 登记会在稍后触发 update 的外部 Promise(响应体旁路采集),让收尾等待能覆盖它。 */
2715
+ trackUpdate(task) {
2716
+ const tracked = task.finally(() => this.pendingUpdates.delete(tracked));
2717
+ this.pendingUpdates.add(tracked);
2718
+ }
2719
+ async getCapacity() {
2720
+ await this.settle();
2721
+ return this.readCapacity();
2722
+ }
2723
+ append(input) {
2724
+ const record = {
2725
+ id: import_koishi2.Random.id(),
2726
+ sequence: this.nextSequence++,
2727
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2728
+ status: input.status,
2729
+ durationMs: input.durationMs,
2730
+ ...input.method ? { method: input.method } : {},
2731
+ ...input.url ? { url: input.url } : {},
2732
+ ...input.provider ? { provider: input.provider } : {},
2733
+ ...input.model ? { model: input.model } : {},
2734
+ ...input.headers ? { headers: structuredClone(input.headers) } : {},
2735
+ attribution: input.attribution,
2736
+ entities: structuredClone(input.entities),
2737
+ requestBodyAvailable: input.requestBodyAvailable,
2738
+ ...input.requestBody !== void 0 ? { requestBody: structuredClone(input.requestBody) } : {},
2739
+ responseBodyStatus: input.responseBodyStatus ?? "unavailable",
2740
+ ...input.responseBodyFormat ? { responseBodyFormat: input.responseBodyFormat } : {},
2741
+ ...input.responseStatus !== void 0 ? { responseStatus: input.responseStatus } : {},
2742
+ ...input.responseBodyRaw !== void 0 ? { responseBodyRaw: input.responseBodyRaw } : {},
2743
+ ...input.responseBodyError ? { responseBodyError: input.responseBodyError } : {},
2744
+ ...input.interactionId ? { interactionId: input.interactionId } : {},
2745
+ ...input.chatlunaRequestId ? { chatlunaRequestId: input.chatlunaRequestId } : {},
2746
+ ...input.presetSnapshots?.length ? { presetSnapshots: structuredClone(input.presetSnapshots) } : {},
2747
+ ...input.error ? { error: structuredClone(input.error) } : {},
2748
+ ...input.chatlunaError ? { chatlunaError: structuredClone(input.chatlunaError) } : {}
2749
+ };
2750
+ this.staged.push(record);
2751
+ this.writes.push(async () => {
2752
+ this.summary = await this.persistence.append(record, estimateModelRequestRecordBytes(record), this.nextSequence);
2753
+ await this.reclaimOverflow();
2754
+ const index = this.staged.indexOf(record);
2755
+ if (index >= 0) this.staged.splice(index, 1);
2756
+ });
2757
+ return structuredClone(record);
2758
+ }
2759
+ update(recordId, input) {
2760
+ return this.writes.run(async () => {
2761
+ const previous = await this.persistence.find(recordId);
2762
+ if (!previous) return;
2763
+ const next = {
2764
+ ...previous,
2765
+ ...input.status ? { status: input.status } : {},
2766
+ ...input.durationMs !== void 0 ? { durationMs: input.durationMs } : {},
2767
+ ...input.headers ? { headers: structuredClone(input.headers) } : {},
2768
+ ...input.responseBodyStatus ? { responseBodyStatus: input.responseBodyStatus } : {},
2769
+ ...input.responseBodyFormat ? { responseBodyFormat: input.responseBodyFormat } : {},
2770
+ ...input.responseStatus !== void 0 ? { responseStatus: input.responseStatus } : {},
2771
+ ...input.responseBodyRaw !== void 0 ? { responseBodyRaw: input.responseBodyRaw } : {},
2772
+ ...input.responseBodyError ? { responseBodyError: input.responseBodyError } : {},
2773
+ ...input.chatlunaRequestId ? { chatlunaRequestId: input.chatlunaRequestId } : {}
2774
+ };
2775
+ if (input.status === "success") delete next.error;
2776
+ else if (input.error) next.error = structuredClone(input.error);
2777
+ if (input.chatlunaError) next.chatlunaError = structuredClone(input.chatlunaError);
2778
+ if (input.responseBodyStatus === "complete") delete next.responseBodyError;
2779
+ this.summary = await this.persistence.replace(next, estimateModelRequestRecordBytes(next));
2780
+ await this.reclaimOverflow();
2781
+ return structuredClone(next);
2782
+ }).catch(() => void 0);
2783
+ }
2784
+ async getRecords(input = {}) {
2785
+ const limit = Math.min(Math.max(Number(input.limit ?? DEFAULT_MODEL_REQUEST_PAGE_SIZE) || DEFAULT_MODEL_REQUEST_PAGE_SIZE, 1), MAX_MODEL_REQUEST_PAGE_SIZE);
2786
+ await this.settle();
2787
+ const earliestCursor = this.summary.earliestSequence;
2788
+ if (input.beforeSequence !== void 0) {
2789
+ if (!Number.isInteger(input.beforeSequence) || input.beforeSequence < 1) {
2790
+ throw new StudioDomainError("beforeSequence \u5FC5\u987B\u662F\u6B63\u6574\u6570");
2791
+ }
2792
+ if (earliestCursor !== void 0 && input.beforeSequence < earliestCursor) {
2793
+ throw new StudioModelRequestCursorExpiredError(
2794
+ `\u6A21\u578B\u8BF7\u6C42\u8BB0\u5F55\u6E38\u6807\u5DF2\u8FC7\u671F\uFF1A${input.beforeSequence}`,
2795
+ earliestCursor
2796
+ );
2797
+ }
2798
+ }
2799
+ const rows = await this.persistence.queryHeaders({
2800
+ ...this.toFilter(input),
2801
+ order: resolveModelRequestOrder(input),
2802
+ ...input.beforeSequence !== void 0 ? { beforeSequence: input.beforeSequence } : {},
2803
+ ...input.beforeCreatedAt ? { beforeCreatedAt: input.beforeCreatedAt } : {},
2804
+ ...input.beforeId ? { beforeId: input.beforeId } : {},
2805
+ limit: limit + 1
2806
+ });
2807
+ const records = rows.slice(0, limit);
2808
+ const hasMore = rows.length > records.length;
2809
+ const last = records[records.length - 1];
2810
+ return {
2811
+ records: records.map((record) => presentModelRequestListItem(record)),
2812
+ hasMore,
2813
+ nextCursor: hasMore ? last?.sequence : void 0,
2814
+ nextCreatedAt: hasMore ? last?.createdAt : void 0,
2815
+ nextId: hasMore ? last?.id : void 0,
2816
+ earliestCursor,
2817
+ capacity: this.readCapacity()
2818
+ };
2819
+ }
2820
+ async getRecord(recordId) {
2821
+ await this.settle();
2822
+ const record = await this.persistence.find(recordId);
2823
+ return record ? presentModelRequestDetail(record) : void 0;
2824
+ }
2825
+ /**
2826
+ * 「这条模型请求记录在不在」由本记录库判定,取不到就抛。
2827
+ *
2828
+ * 与 `getRecord` 并存而不是替代它:本成员服务于「调用方就是要这条记录」,`getRecord` 服务于
2829
+ * 跨记录域遍历——遍历必须能区分「这个记录域里没有」与「这个记录域的持久化坏了」,用异常表达
2830
+ * 未命中会让一次真实故障被当成「这里没有」静默跳过。
2831
+ */
2832
+ async requireRecord(recordId) {
2833
+ const record = await this.getRecord(recordId);
2834
+ if (!record) throw new StudioDomainError(`\u6A21\u578B\u8BF7\u6C42\u8BB0\u5F55\u4E0D\u5B58\u5728\uFF1A${recordId}`);
2835
+ return record;
2836
+ }
2837
+ async getRawRecords(input = {}) {
2838
+ const limit = Math.min(Math.max(Number(input.limit ?? MAX_MODEL_REQUEST_PAGE_SIZE) || MAX_MODEL_REQUEST_PAGE_SIZE, 1), MAX_MODEL_REQUEST_PAGE_SIZE);
2839
+ await this.settle();
2840
+ return this.persistence.query({
2841
+ ...this.toFilter(input),
2842
+ order: resolveModelRequestOrder(input),
2843
+ limit
2844
+ });
2845
+ }
2846
+ clear() {
2847
+ return this.writes.run(async () => {
2848
+ const cleared = this.summary.recordCount;
2849
+ this.summary = await this.persistence.clear(this.nextSequence);
2850
+ return cleared;
2851
+ }).catch(() => 0);
2852
+ }
2853
+ /**
2854
+ * 筛选下拉的可选值。
2855
+ *
2856
+ * 只扫最近一段记录头而不是全表:这是一份用于「点开筛选看看有哪些会话」的辅助清单,
2857
+ * 而记录库的保留上限本来就会淘汰更早的记录,为它引入一条新的存储查询不划算。
2858
+ */
2859
+ async getFacets(limit = MODEL_REQUEST_FACET_SCAN_LIMIT) {
2860
+ await this.settle();
2861
+ const rows = await this.persistence.queryHeaders({ order: "desc", limit });
2862
+ const bots = /* @__PURE__ */ new Map();
2863
+ const conversations = /* @__PURE__ */ new Map();
2864
+ const models = /* @__PURE__ */ new Set();
2865
+ for (const { entities, model } of rows) {
2866
+ if (model) models.add(model);
2867
+ if (entities.botId && !bots.has(entities.botId)) {
2868
+ bots.set(entities.botId, {
2869
+ id: entities.botId,
2870
+ ...entities.botName ? { name: entities.botName } : {},
2871
+ ...entities.platform ? { platform: entities.platform } : {}
2872
+ });
2873
+ }
2874
+ if (entities.conversationId && !conversations.has(entities.conversationId)) {
2875
+ conversations.set(entities.conversationId, {
2876
+ id: entities.conversationId,
2877
+ ...entities.conversationName ? { name: entities.conversationName } : {},
2878
+ ...entities.conversationType ? { type: entities.conversationType } : {},
2879
+ ...entities.botId ? { botId: entities.botId } : {}
2880
+ });
2881
+ }
2882
+ }
2883
+ return {
2884
+ bots: [...bots.values()],
2885
+ conversations: [...conversations.values()],
2886
+ models: [...models].sort((left, right) => left.localeCompare(right))
2887
+ };
2888
+ }
2889
+ toFilter(input) {
2890
+ return {
2891
+ ...input.attribution ? { attribution: input.attribution } : {},
2892
+ ...input.botId ? { botId: input.botId } : {},
2893
+ ...input.conversationId ? { conversationId: input.conversationId } : {},
2894
+ ...input.interactionId ? { interactionId: input.interactionId } : {},
2895
+ ...input.model ? { model: input.model } : {},
2896
+ ...input.errorsOnly ? { errorsOnly: true } : {}
2897
+ };
2898
+ }
2899
+ readCapacity() {
2900
+ return {
2901
+ recordCount: this.summary.recordCount,
2902
+ totalBytes: this.summary.totalBytes,
2903
+ maxRecords: this.maxRecords,
2904
+ maxBytes: this.maxBytes
2905
+ };
2906
+ }
2907
+ async reclaimOverflow() {
2908
+ if (this.summary.recordCount <= this.maxRecords && this.summary.totalBytes <= this.maxBytes) return;
2909
+ this.summary = await this.persistence.reclaim({ maxRecords: this.maxRecords, maxBytes: this.maxBytes });
2910
+ }
2911
+ };
2912
+ function resolveModelRequestOrder(input) {
2913
+ return input.order === "asc" ? "asc" : "desc";
2914
+ }
2915
+
2916
+ // src/evidence-preview-text.ts
2917
+ var SCAN_CHUNK = 4096;
2918
+ var SCAN_GROWTH = 4;
2919
+ function normalizeEvidencePreviewText(value, minLength) {
2920
+ let scan = Math.min(value.length, Math.max(minLength * SCAN_GROWTH, SCAN_CHUNK));
2921
+ let normalized = normalize(value, scan);
2922
+ while (normalized.length <= minLength && scan < value.length) {
2923
+ scan = Math.min(value.length, scan * SCAN_GROWTH);
2924
+ normalized = normalize(value, scan);
2925
+ }
2926
+ return normalized;
2927
+ }
2928
+ function normalize(value, scan) {
2929
+ return (scan >= value.length ? value : value.slice(0, scan)).replace(/\s+/g, " ").trim();
2930
+ }
2931
+
2932
+ // src/evidence-kind.ts
2933
+ var EVIDENCE_KIND_LABELS = {
2934
+ system: { badge: "SYSTEM", title: "System" },
2935
+ user: { badge: "USER", title: "User" },
2936
+ assistant: { badge: "ASSISTANT", title: "Assistant" },
2937
+ variable: { badge: "VARIABLE", title: "Variable" },
2938
+ "tool-definition": { badge: "TOOL DEFS", title: "Tool Defs" },
2939
+ "tool-call": { badge: "TOOL CALL", title: "Tool Call" },
2940
+ "tool-result": { badge: "TOOL RESULT", title: "Tool Result" },
2941
+ response: { badge: "RESPONSE", title: "Response" }
2942
+ };
2943
+ var EVIDENCE_AGGREGATE_MEMBERS = {
2944
+ "tool-interaction": Object.freeze(["tool-call", "tool-result"]),
2945
+ tool: Object.freeze(["tool-definition", "tool-call", "tool-result"])
2946
+ };
2947
+ var EVIDENCE_AGGREGATE_LABELS = {
2948
+ "tool-interaction": { badge: "TOOL I/O", title: "Tool I/O" },
2949
+ tool: { badge: "TOOL", title: "Tool" }
2950
+ };
2951
+ var STUDIO_EVIDENCE_KINDS = Object.freeze(
2952
+ Object.keys(EVIDENCE_KIND_LABELS)
2953
+ );
2954
+ var STUDIO_EVIDENCE_AGGREGATE_IDS = Object.freeze(
2955
+ Object.keys(EVIDENCE_AGGREGATE_MEMBERS)
2956
+ );
2957
+ function studioEvidenceLabels(id) {
2958
+ return isAggregateId(id) ? EVIDENCE_AGGREGATE_LABELS[id] : EVIDENCE_KIND_LABELS[id];
2959
+ }
2960
+ function studioEvidenceAggregate(id) {
2961
+ return {
2962
+ id,
2963
+ members: EVIDENCE_AGGREGATE_MEMBERS[id],
2964
+ labels: EVIDENCE_AGGREGATE_LABELS[id]
2965
+ };
2966
+ }
2967
+ function isStudioEvidenceAggregateMember(aggregate, kind) {
2968
+ return EVIDENCE_AGGREGATE_MEMBERS[aggregate].includes(kind);
2969
+ }
2970
+ function isAggregateId(id) {
2971
+ return id in EVIDENCE_AGGREGATE_LABELS;
2972
+ }
2973
+
2974
+ // src/model-request-trajectory.ts
2975
+ var CONVERSATION_RECORD_LIMIT = 200;
2976
+ var TRAJECTORY_PREVIEW_LENGTH = 180;
2977
+ var TOOL_INTERACTION_KIND = studioEvidenceAggregate("tool-interaction").id;
2978
+ async function readStudioModelRequestConversationRecords(record, store) {
2979
+ const conversationId = record.entities.conversationId;
2980
+ if (!store || !conversationId) return;
2981
+ return store.getRawRecords({ conversationId, order: "asc", limit: CONVERSATION_RECORD_LIMIT });
2982
+ }
2983
+ async function buildStudioModelRequestTrajectoryFromStore(options) {
2984
+ const conversationRecords = options.mode === "conversation" ? await readStudioModelRequestConversationRecords(options.record, options.store) : void 0;
2985
+ return buildStudioModelRequestTrajectory({
2986
+ record: options.record,
2987
+ mode: options.mode,
2988
+ ...conversationRecords ? { conversationRecords } : {}
2989
+ });
2990
+ }
2991
+ function buildStudioModelRequestTrajectory(options) {
2992
+ const sourceRecords = options.mode === "conversation" && options.conversationRecords ? options.conversationRecords : [options.record];
2993
+ const records = sourceRecords.map((record) => presentModelRequestListItem(record));
2994
+ const rows = [];
2995
+ const promptComposition = [];
2996
+ let index = 1;
2997
+ for (const record of sourceRecords) {
2998
+ const projection = projectModelEvidence({
2999
+ requestBody: record.requestBody,
3000
+ ...record.responseBodyStatus === "complete" && record.responseBodyRaw !== void 0 ? { responseBodyRaw: record.responseBodyRaw, responseBodyFormat: record.responseBodyFormat } : {}
3001
+ });
3002
+ const variables = record.requestBody !== void 0 && record.presetSnapshots?.length ? deriveModelRequestVariables(record.presetSnapshots, projection) : [];
3003
+ rows.push({
3004
+ id: `${record.id}:request`,
3005
+ index: index++,
3006
+ kind: "request",
3007
+ preview: requestPreview(record),
3008
+ durationMs: record.durationMs,
3009
+ startedAt: record.createdAt,
3010
+ requestId: record.id,
3011
+ status: record.status
3012
+ });
3013
+ for (const row of projectRequestRows(projection, variables)) {
3014
+ rows.push({
3015
+ id: `${record.id}:${row.evidenceId}`,
3016
+ index: index++,
3017
+ requestId: record.id,
3018
+ source: "request",
3019
+ ...row
3020
+ });
3021
+ }
3022
+ for (const row of projectResponseRows(projection)) {
3023
+ rows.push({ id: `${record.id}:${row.evidenceId}`, index: index++, requestId: record.id, source: "response", ...row });
3024
+ }
3025
+ for (const item of projectPromptComposition(projection, variables)) {
3026
+ promptComposition.push(options.mode === "conversation" ? { ...item, requestId: record.id } : item);
3027
+ }
3028
+ }
3029
+ return {
3030
+ mode: options.mode,
3031
+ ...options.record.entities.conversationId ? { conversationId: options.record.entities.conversationId } : {},
3032
+ records,
3033
+ rows,
3034
+ promptComposition,
3035
+ complete: options.mode === "request" || !options.conversationRecords || sourceRecords.length < CONVERSATION_RECORD_LIMIT
3036
+ };
3037
+ }
3038
+ function projectRequestRows(projection, variables) {
3039
+ const rows = projection.toolDefinitions.map((definition) => ({
3040
+ evidenceId: definition.evidenceId,
3041
+ kind: "tool-definition",
3042
+ preview: `\u5DE5\u5177\u5B9A\u4E49 \xB7 ${definition.name}`,
3043
+ toolName: definition.name
3044
+ }));
3045
+ for (const message of projection.requestMessages) {
3046
+ rows.push(...messageRows(message));
3047
+ }
3048
+ for (const variable of variables) {
3049
+ rows.push({
3050
+ evidenceId: `variable:${variable.id}`,
3051
+ kind: "variable",
3052
+ preview: `${variable.name} \xB7 ${variable.status === "observed" ? compactText(variable.value ?? "") || "\u7A7A\u503C" : modelRequestVariableStatusLabel(variable.status)}`,
3053
+ variableId: variable.id,
3054
+ variableName: variable.name,
3055
+ variablePresetName: variable.presetName,
3056
+ variableStatus: variable.status,
3057
+ ...variable.value !== void 0 ? { variableValue: variable.value } : {}
3058
+ });
3059
+ }
3060
+ return rows.sort((left, right) => requestRowOrder(left.kind) - requestRowOrder(right.kind));
3061
+ }
3062
+ function requestRowOrder(kind) {
3063
+ if (kind === "system") return 0;
3064
+ if (kind === "user") return 1;
3065
+ if (kind === "variable") return 2;
3066
+ if (kind === "assistant") return 3;
3067
+ return 4;
3068
+ }
3069
+ function messageRows(message) {
3070
+ const rows = [];
3071
+ if (message.role === "tool") {
3072
+ rows.push({
3073
+ evidenceId: message.evidenceId,
3074
+ kind: "tool-result",
3075
+ preview: `${message.toolName ?? "\u5DE5\u5177\u7ED3\u679C"} \xB7 ${compactText(message.text) || "\u65E0\u8F93\u51FA"}`,
3076
+ ...message.toolName ? { toolName: message.toolName } : {},
3077
+ ...message.toolCallId ? { callId: message.toolCallId } : {}
3078
+ });
3079
+ return rows;
3080
+ }
3081
+ const reasoning = message.reasoning ? compactText(message.reasoning) : "";
3082
+ const text = compactText(message.text);
3083
+ if (text || reasoning) {
3084
+ rows.push({
3085
+ evidenceId: message.evidenceId,
3086
+ kind: message.role,
3087
+ preview: text || `\u601D\u8003 \xB7 ${reasoning}`
3088
+ });
3089
+ }
3090
+ for (const call of message.toolCalls) {
3091
+ rows.push({
3092
+ evidenceId: call.evidenceId,
3093
+ kind: "tool-call",
3094
+ preview: `${call.name} \xB7 ${compactText(call.arguments ?? "") || "\u65E0\u53C2\u6570"}`,
3095
+ toolName: call.name,
3096
+ ...call.callId ? { callId: call.callId } : {}
3097
+ });
3098
+ }
3099
+ return rows;
3100
+ }
3101
+ function projectResponseRows(projection) {
3102
+ return projection.responseEvents.flatMap((event) => {
3103
+ if (event.kind === "reasoning") {
3104
+ return [{ evidenceId: event.evidenceId, kind: "assistant", preview: `\u601D\u8003 \xB7 ${compactText(event.text ?? "")}` }];
3105
+ }
3106
+ if (event.kind === "content") {
3107
+ return [{ evidenceId: event.evidenceId, kind: "assistant", preview: compactText(event.text ?? "") }];
3108
+ }
3109
+ if (event.kind === "tool-call") {
3110
+ return [{
3111
+ evidenceId: event.evidenceId,
3112
+ kind: "tool-call",
3113
+ preview: `${event.name ?? "\u5DE5\u5177\u8C03\u7528"} \xB7 ${compactText(event.arguments ?? "") || "\u65E0\u53C2\u6570"}`,
3114
+ ...event.name ? { toolName: event.name } : {},
3115
+ ...event.callId ? { callId: event.callId } : {}
3116
+ }];
3117
+ }
3118
+ if (event.kind === "tool-result") {
3119
+ return [{
3120
+ evidenceId: event.evidenceId,
3121
+ kind: "tool-result",
3122
+ preview: `${event.name ?? "\u5DE5\u5177\u7ED3\u679C"} \xB7 ${compactText(event.text ?? "") || "\u65E0\u8F93\u51FA"}`,
3123
+ ...event.name ? { toolName: event.name } : {},
3124
+ ...event.callId ? { callId: event.callId } : {}
3125
+ }];
3126
+ }
3127
+ return [];
3128
+ });
3129
+ }
3130
+ function projectPromptComposition(projection, variables) {
3131
+ const sequence = [];
3132
+ const variablesByEvidenceId = groupObservedVariablesByEvidenceId(variables);
3133
+ for (const message of projection.requestMessages) {
3134
+ const kind = message.role === "tool" ? TOOL_INTERACTION_KIND : message.role;
3135
+ const messageCharacters = countMessageCharacters(message);
3136
+ if (kind === "system" || kind === "user") {
3137
+ pushMessageComposition(sequence, kind, message.evidenceId, messageCharacters, variablesByEvidenceId.get(message.evidenceId) ?? []);
3138
+ } else {
3139
+ push(sequence, kind, message.evidenceId, messageCharacters);
3140
+ }
3141
+ for (const call of message.toolCalls) {
3142
+ push(sequence, TOOL_INTERACTION_KIND, call.evidenceId, countToolCallCharacters(call));
3143
+ }
3144
+ }
3145
+ const tools = projection.toolDefinitions.map((definition) => ({
3146
+ kind: "tool-definition",
3147
+ evidenceId: definition.evidenceId,
3148
+ characters: Math.max(countToolDefinitionCharacters(definition), 1)
3149
+ }));
3150
+ let split = 0;
3151
+ while (split < sequence.length && (sequence[split].kind === "system" || sequence[split].kind === "user")) {
3152
+ split += 1;
3153
+ }
3154
+ return [...sequence.slice(0, split), ...tools, ...sequence.slice(split)];
3155
+ }
3156
+ function groupObservedVariablesByEvidenceId(variables) {
3157
+ const grouped = /* @__PURE__ */ new Map();
3158
+ for (const variable of variables) {
3159
+ if (variable.status !== "observed" || !variable.evidenceId || !variable.range) continue;
3160
+ if (variable.range.end <= variable.range.start) continue;
3161
+ const items = grouped.get(variable.evidenceId) ?? [];
3162
+ items.push(variable);
3163
+ grouped.set(variable.evidenceId, items);
3164
+ }
3165
+ for (const items of grouped.values()) {
3166
+ items.sort((left, right) => left.range.start - right.range.start || left.range.end - right.range.end);
3167
+ }
3168
+ return grouped;
3169
+ }
3170
+ function pushMessageComposition(target, kind, evidenceId2, characters, variables) {
3171
+ if (!variables.length || characters <= 0) {
3172
+ push(target, kind, evidenceId2, characters);
3173
+ return;
3174
+ }
3175
+ let offset = 0;
3176
+ for (const variable of variables) {
3177
+ const range = variable.range;
3178
+ if (range.start < offset || range.start > characters || range.end > characters) continue;
3179
+ push(target, kind, evidenceId2, range.start - offset);
3180
+ target.push({
3181
+ // 模型请求变量在组成图中统一投影到 User 轨道,和工具定义一样按变量逐段展示。
3182
+ kind: "user",
3183
+ evidenceId: `variable:${variable.id}`,
3184
+ characters: range.end - range.start,
3185
+ variableId: variable.id,
3186
+ variableName: variable.name
3187
+ });
3188
+ offset = range.end;
3189
+ }
3190
+ push(target, kind, evidenceId2, characters - offset);
3191
+ }
3192
+ function push(target, kind, evidenceId2, count) {
3193
+ if (count > 0) target.push({ kind, evidenceId: evidenceId2, characters: count });
3194
+ }
3195
+ function requestPreview(record) {
3196
+ const provider = record.provider || "\u672A\u77E5\u6E20\u9053";
3197
+ const model = record.model || "\u672A\u77E5\u6A21\u578B";
3198
+ if (record.status === "pending") return `${provider} / ${model} \xB7 \u8FDB\u884C\u4E2D`;
3199
+ if (record.status === "error") return `${provider} / ${model} \xB7 \u8BF7\u6C42\u5931\u8D25`;
3200
+ return `${provider} / ${model} \xB7 ${record.durationMs} ms`;
3201
+ }
3202
+ function compactText(value) {
3203
+ const normalized = normalizeEvidencePreviewText(value, TRAJECTORY_PREVIEW_LENGTH);
3204
+ return normalized.length > TRAJECTORY_PREVIEW_LENGTH ? `${normalized.slice(0, 177)}\u2026` : normalized;
3205
+ }
3206
+ function statusLabel(status) {
3207
+ if (status === "pending") return "\u8FDB\u884C\u4E2D";
3208
+ if (status === "error") return "\u9519\u8BEF";
3209
+ return "\u5DF2\u5B8C\u6210";
3210
+ }
3211
+
3212
+ // src/console.ts
3213
+ function resolveConsoleEntry(workspace = process.cwd()) {
3214
+ const installedRoot = (0, import_node_path.resolve)(workspace, "node_modules/koishi-plugin-chatluna-studio");
3215
+ const packageRoot = (0, import_node_fs.existsSync)((0, import_node_path.resolve)(installedRoot, "package.json")) ? installedRoot : (0, import_node_path.resolve)(__dirname, "..");
3216
+ return {
3217
+ dev: (0, import_node_path.resolve)(packageRoot, "client/index.ts"),
3218
+ prod: (0, import_node_path.resolve)(packageRoot, "dist")
3219
+ };
3220
+ }
3221
+ function registerConsole(console, options) {
3222
+ const { modelRequests, presets, appearance } = options;
3223
+ console.addEntry(resolveConsoleEntry());
3224
+ const getWorkspace = async () => ({
3225
+ appearance,
3226
+ persistence: options.getPersistenceStatus()
3227
+ });
3228
+ const listRecords = (input) => {
3229
+ const { scope, ...query } = input;
3230
+ return modelRequests.getRecords(scope === "all" ? query : { ...query, attribution: scope });
3231
+ };
3232
+ const readRecord4 = async (input) => {
3233
+ const detail = await modelRequests.requireRecord(input.recordId);
3234
+ const usage2 = await lookupChatLunaUsage(options.chatlunaUsage?.(), detail);
3235
+ return usage2 ? { ...detail, usage: usage2 } : detail;
3236
+ };
3237
+ const readTrajectory = async (input) => {
3238
+ const record = await modelRequests.requireRecord(input.recordId);
3239
+ return buildStudioModelRequestTrajectoryFromStore({ record, mode: input.mode, store: modelRequests });
3240
+ };
3241
+ const clearRecords = async () => ({
3242
+ cleared: await modelRequests.clear()
3243
+ });
3244
+ const registerListener = console.addListener.bind(console);
3245
+ registerListener("chatluna-studio/workspace", getWorkspace, { authority: 4 });
3246
+ registerListener("chatluna-studio/model-request-records", listRecords, { authority: 4 });
3247
+ registerListener("chatluna-studio/model-request-record", readRecord4, { authority: 4 });
3248
+ registerListener("chatluna-studio/model-request-trajectory", readTrajectory, { authority: 4 });
3249
+ registerListener("chatluna-studio/model-request-facets", () => modelRequests.getFacets(), { authority: 4 });
3250
+ registerListener("chatluna-studio/clear-model-request-records", clearRecords, { authority: 4 });
3251
+ registerListener("chatluna-studio/preset-catalog", (input = {}) => presets.catalog(input.kind), { authority: 4 });
3252
+ registerListener("chatluna-studio/preset-read", (input) => presets.read(input), { authority: 4 });
3253
+ registerListener("chatluna-studio/preset-create", (input) => presets.create(input), { authority: 4 });
3254
+ registerListener("chatluna-studio/preset-save", (input) => presets.save(input), { authority: 4 });
3255
+ registerListener("chatluna-studio/preset-rename", (input) => presets.rename(input), { authority: 4 });
3256
+ registerListener("chatluna-studio/preset-delete", (input) => presets.delete(input), { authority: 4 });
3257
+ registerListener("chatluna-studio/preset-locate-expression", (input) => presets.locateExpression(input), { authority: 4 });
3258
+ return (sequence) => {
3259
+ void console.broadcast("chatluna-studio/model-request-recorded", { sequence });
3260
+ };
3261
+ }
3262
+
3263
+ // src/model-request-collector.ts
3264
+ var import_node_module = require("module");
3265
+ var import_node_diagnostics_channel = __toESM(require("diagnostics_channel"));
3266
+ var import_node_path2 = require("path");
3267
+ var AUTH_QUERY_PATTERN = /^(?:api[_-]?key|key|token|access[_-]?token|auth(?:orization)?|secret|password|signature)$/i;
3268
+ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
3269
+ function isKnownChatModelRequestUrl(url) {
3270
+ const path = readUrlPath(url);
3271
+ return /\/chat\/completions\/?$/i.test(path) || /\/responses\/?$/i.test(path) || /\/v1\/messages\/?$/i.test(path) || /:generateContent$/i.test(path) || /:streamGenerateContent$/i.test(path);
3272
+ }
3273
+ function sanitizeModelRequestUrl(raw) {
3274
+ try {
3275
+ const url = new URL(raw);
3276
+ url.username = "";
3277
+ url.password = "";
3278
+ for (const key of [...url.searchParams.keys()]) {
3279
+ if (AUTH_QUERY_PATTERN.test(key)) url.searchParams.delete(key);
3280
+ }
3281
+ return url.toString();
3282
+ } catch {
3283
+ return raw.split("?")[0] ?? raw;
3284
+ }
3285
+ }
3286
+ function resolveChatLunaPluginClass(baseDir = process.cwd()) {
3287
+ const runtimeRequire = (0, import_node_module.createRequire)((0, import_node_path2.resolve)(baseDir, "package.json"));
3288
+ try {
3289
+ const loaded = runtimeRequire("koishi-plugin-chatluna/services/chat");
3290
+ if (typeof loaded.ChatLunaPlugin?.prototype?.fetch === "function") return loaded.ChatLunaPlugin;
3291
+ } catch {
3292
+ }
3293
+ }
3294
+ var pendingDispatchedRequests = [];
3295
+ var undiciRequestCollectorReferences = 0;
3296
+ var undiciRequestCollectorSubscribed = false;
3297
+ function captureDispatchedModelRequest(message) {
3298
+ const request = message && typeof message === "object" ? Reflect.get(message, "request") : void 0;
3299
+ if (!request) return;
3300
+ const method = String(request.method ?? "GET").toUpperCase();
3301
+ const rawUrl = `${String(request.origin ?? "")}${String(request.path ?? "")}`;
3302
+ if (!rawUrl || !isKnownChatModelRequestUrl(rawUrl)) return;
3303
+ const index = pendingDispatchedRequests.findIndex((pending2) => pending2.method === method && urlsReferToSameRequest(pending2.url, rawUrl));
3304
+ if (index < 0) return;
3305
+ const pending = pendingDispatchedRequests.splice(index, 1)[0];
3306
+ const rawHeaders = message && typeof message === "object" ? Reflect.get(message, "headers") : void 0;
3307
+ const headers = readDispatchedRequestHeaders(request, rawHeaders);
3308
+ if (Object.keys(headers).length) pending.store.update(pending.recordId, { headers });
3309
+ }
3310
+ function subscribeUndiciRequestCollector() {
3311
+ undiciRequestCollectorReferences += 1;
3312
+ const channel = import_node_diagnostics_channel.default.channel("undici:client:sendHeaders");
3313
+ if (!undiciRequestCollectorSubscribed) {
3314
+ channel.subscribe(captureDispatchedModelRequest);
3315
+ undiciRequestCollectorSubscribed = true;
3316
+ }
3317
+ return () => {
3318
+ undiciRequestCollectorReferences -= 1;
3319
+ if (undiciRequestCollectorReferences > 0 || !undiciRequestCollectorSubscribed) return;
3320
+ channel.unsubscribe(captureDispatchedModelRequest);
3321
+ undiciRequestCollectorSubscribed = false;
3322
+ pendingDispatchedRequests.splice(0);
3323
+ };
3324
+ }
3325
+ function inferModelResponseBodyFormat(contentType, raw) {
3326
+ if (contentType?.toLowerCase().includes("text/event-stream") || /^(?:event|data|id|retry):/m.test(raw)) return "sse";
3327
+ try {
3328
+ JSON.parse(raw);
3329
+ return "json";
3330
+ } catch {
3331
+ return "text";
3332
+ }
3333
+ }
3334
+ function installModelRequestCollector(options) {
3335
+ const plugin = options.plugin ?? resolveChatLunaPluginClass(options.baseDir);
3336
+ if (!plugin) return () => {
3337
+ };
3338
+ const original = plugin.prototype.fetch;
3339
+ if (typeof original !== "function") return () => {
3340
+ };
3341
+ const unsubscribeUndiciRequestCollector = subscribeUndiciRequestCollector();
3342
+ const wrapped = async function wrappedModelRequestFetch(info, init, proxy) {
3343
+ const request = readRequest(info, init);
3344
+ if (!request.url || !MUTATING_METHODS.has(request.method) || !isKnownChatModelRequestUrl(request.url)) {
3345
+ return original.call(this, info, init, proxy);
3346
+ }
3347
+ const resolution = options.resolveTurn();
3348
+ if (resolution.virtualOnly) return original.call(this, info, init, proxy);
3349
+ const body = parseJsonBody(request.body);
3350
+ const store = options.store;
3351
+ const turn = resolution.turn;
3352
+ const startedAt = Date.now();
3353
+ const configuredHeaders = readRequestHeaders(info, init);
3354
+ const presetSnapshots = turn?.entities.botId && turn.entities.conversationId ? options.getActivePresetSnapshots?.({
3355
+ botId: turn.entities.botId,
3356
+ conversationId: turn.entities.conversationId
3357
+ }) : void 0;
3358
+ const pending = store.append({
3359
+ status: "pending",
3360
+ durationMs: 0,
3361
+ method: request.method,
3362
+ url: sanitizeModelRequestUrl(request.url),
3363
+ ...inferProvider(request.url) ? { provider: inferProvider(request.url) } : {},
3364
+ ...inferModel(body.value, request.url) ? { model: inferModel(body.value, request.url) } : {},
3365
+ ...Object.keys(configuredHeaders).length ? { headers: configuredHeaders } : {},
3366
+ attribution: turn ? "attributed" : "unattributed",
3367
+ entities: turn?.entities ?? {},
3368
+ ...turn ? { interactionId: turn.interactionId } : {},
3369
+ requestBodyAvailable: body.available,
3370
+ ...body.available ? { requestBody: body.value } : {},
3371
+ ...presetSnapshots?.length ? { presetSnapshots } : {},
3372
+ responseBodyStatus: "pending"
3373
+ });
3374
+ options.onRecordAppended?.(pending);
3375
+ pendingDispatchedRequests.push({ method: request.method, url: request.url, recordId: pending.id, store });
3376
+ try {
3377
+ const response = await original.call(this, info, init, proxy);
3378
+ const durationMs = Date.now() - startedAt;
3379
+ const responseStatus = typeof response?.status === "number" ? response.status : void 0;
3380
+ const capture = prepareModelResponseCapture(response);
3381
+ if (response && typeof response === "object" && response.ok === false) {
3382
+ store.update(pending.id, {
3383
+ status: "error",
3384
+ durationMs,
3385
+ responseBodyStatus: capture ? "pending" : "unavailable",
3386
+ ...responseStatus !== void 0 ? { responseStatus } : {},
3387
+ error: createModelRequestError(new Error(`HTTP ${response.status ?? "error"}`))
3388
+ });
3389
+ } else {
3390
+ store.update(pending.id, {
3391
+ status: "success",
3392
+ durationMs,
3393
+ responseBodyStatus: capture ? "pending" : "unavailable",
3394
+ ...responseStatus !== void 0 ? { responseStatus } : {}
3395
+ });
3396
+ }
3397
+ if (capture) {
3398
+ store.trackUpdate(capture.then(({ raw, format }) => {
3399
+ store.update(pending.id, {
3400
+ durationMs: Date.now() - startedAt,
3401
+ responseBodyStatus: "complete",
3402
+ responseBodyFormat: format,
3403
+ responseBodyRaw: raw
3404
+ });
3405
+ }).catch((error) => {
3406
+ store.update(pending.id, {
3407
+ durationMs: Date.now() - startedAt,
3408
+ responseBodyStatus: "error",
3409
+ responseBodyError: error instanceof Error ? error.message : String(error)
3410
+ });
3411
+ }));
3412
+ }
3413
+ return response;
3414
+ } catch (error) {
3415
+ store.update(pending.id, {
3416
+ status: "error",
3417
+ durationMs: Date.now() - startedAt,
3418
+ responseBodyStatus: "unavailable",
3419
+ error: createModelRequestError(error)
3420
+ });
3421
+ throw error;
3422
+ }
3423
+ };
3424
+ plugin.prototype.fetch = wrapped;
3425
+ return () => {
3426
+ if (plugin.prototype.fetch === wrapped) plugin.prototype.fetch = original;
3427
+ unsubscribeUndiciRequestCollector();
3428
+ };
3429
+ }
3430
+ function prepareModelResponseCapture(response) {
3431
+ if (!response || typeof response.clone !== "function" || response.bodyUsed) return;
3432
+ try {
3433
+ const clone = response.clone();
3434
+ if (typeof clone.text !== "function") return;
3435
+ const contentType = clone.headers?.get?.("content-type") ?? response.headers?.get?.("content-type") ?? void 0;
3436
+ return clone.text().then((raw) => ({
3437
+ raw,
3438
+ format: inferModelResponseBodyFormat(contentType ?? void 0, raw)
3439
+ }));
3440
+ } catch {
3441
+ return;
3442
+ }
3443
+ }
3444
+ function readUrlPath(raw) {
3445
+ try {
3446
+ return new URL(raw).pathname;
3447
+ } catch {
3448
+ return raw.split("?")[0] ?? raw;
3449
+ }
3450
+ }
3451
+ function readRequest(info, init) {
3452
+ const url = typeof info === "string" ? info : info instanceof URL ? info.toString() : info && typeof info === "object" && typeof Reflect.get(info, "url") === "string" ? String(Reflect.get(info, "url")) : "";
3453
+ const method = String(
3454
+ init?.method ?? (info && typeof info === "object" ? Reflect.get(info, "method") : void 0) ?? "GET"
3455
+ ).toUpperCase();
3456
+ const body = init?.body ?? (info && typeof info === "object" && typeof Reflect.get(info, "body") === "string" ? Reflect.get(info, "body") : void 0);
3457
+ return { url, method, body };
3458
+ }
3459
+ function readRequestHeaders(info, init) {
3460
+ const source2 = init?.headers ?? (info && typeof info === "object" ? Reflect.get(info, "headers") : void 0);
3461
+ if (!source2) return {};
3462
+ const result = {};
3463
+ if (typeof Headers !== "undefined" && source2 instanceof Headers) {
3464
+ source2.forEach((value, key) => {
3465
+ result[key] = value;
3466
+ });
3467
+ return result;
3468
+ }
3469
+ if (Array.isArray(source2)) {
3470
+ for (const entry of source2) {
3471
+ if (Array.isArray(entry) && entry.length >= 2) result[String(entry[0])] = String(entry[1]);
3472
+ }
3473
+ return result;
3474
+ }
3475
+ if (typeof source2 === "object") {
3476
+ for (const [key, value] of Object.entries(source2)) {
3477
+ if (value !== void 0) result[key] = Array.isArray(value) ? value.join(", ") : String(value);
3478
+ }
3479
+ }
3480
+ return result;
3481
+ }
3482
+ function readDispatchedRequestHeaders(request, rawHeaders) {
3483
+ const result = typeof rawHeaders === "string" ? parseRawHttpRequestHeaders(rawHeaders) : {};
3484
+ if (!Object.keys(result).length && Array.isArray(request.headers)) {
3485
+ for (let index = 0; index + 1 < request.headers.length; index += 2) {
3486
+ result[String(request.headers[index])] = String(request.headers[index + 1]);
3487
+ }
3488
+ }
3489
+ if (typeof request.contentLength === "number" && request.contentLength >= 0 && !hasHeader(result, "content-length")) {
3490
+ result["Content-Length"] = String(request.contentLength);
3491
+ }
3492
+ return result;
3493
+ }
3494
+ function parseRawHttpRequestHeaders(raw) {
3495
+ const result = {};
3496
+ for (const line of raw.split("\r\n").slice(1)) {
3497
+ const separator = line.indexOf(":");
3498
+ if (separator <= 0) continue;
3499
+ const key = line.slice(0, separator).trim();
3500
+ const value = line.slice(separator + 1).trim();
3501
+ if (key) result[key] = value;
3502
+ }
3503
+ return result;
3504
+ }
3505
+ function hasHeader(headers, expected) {
3506
+ return Object.keys(headers).some((key) => key.toLowerCase() === expected);
3507
+ }
3508
+ function urlsReferToSameRequest(left, right) {
3509
+ try {
3510
+ const leftUrl = new URL(left);
3511
+ const rightUrl = new URL(right);
3512
+ return leftUrl.origin === rightUrl.origin && leftUrl.pathname === rightUrl.pathname && leftUrl.search === rightUrl.search;
3513
+ } catch {
3514
+ return left === right;
3515
+ }
3516
+ }
3517
+ function parseJsonBody(body) {
3518
+ if (body == null) return { available: false };
3519
+ let text;
3520
+ if (typeof body === "string") text = body;
3521
+ else if (typeof Buffer !== "undefined" && Buffer.isBuffer(body)) text = body.toString("utf8");
3522
+ else if (body instanceof Uint8Array) text = Buffer.from(body).toString("utf8");
3523
+ else if (body instanceof ArrayBuffer) text = Buffer.from(body).toString("utf8");
3524
+ else return { available: false };
3525
+ try {
3526
+ return { available: true, value: JSON.parse(text) };
3527
+ } catch {
3528
+ return { available: false };
3529
+ }
3530
+ }
3531
+ function inferModel(body, rawUrl) {
3532
+ if (body && typeof body === "object") {
3533
+ const directModel = Reflect.get(body, "model");
3534
+ if (typeof directModel === "string" && directModel) return directModel;
3535
+ const modelVersion = Reflect.get(body, "modelVersion");
3536
+ if (typeof modelVersion === "string" && modelVersion) return modelVersion;
3537
+ }
3538
+ if (rawUrl) {
3539
+ try {
3540
+ const match = new URL(rawUrl).pathname.match(/\/models\/([^/:]+)(?::|$)/i);
3541
+ if (match?.[1]) return decodeURIComponent(match[1]);
3542
+ } catch {
3543
+ }
3544
+ }
3545
+ }
3546
+ function inferProvider(url) {
3547
+ try {
3548
+ const host = new URL(url).hostname.toLowerCase();
3549
+ if (host.includes("openai")) return "openai";
3550
+ if (host.includes("anthropic")) return "anthropic";
3551
+ if (host.includes("googleapis") || host.includes("generativelanguage")) return "gemini";
3552
+ if (host.includes("openrouter")) return "openrouter";
3553
+ return host || void 0;
3554
+ } catch {
3555
+ return void 0;
3556
+ }
3557
+ }
3558
+
3559
+ // src/persistence.ts
3560
+ var MODEL_REQUEST_RECORD_TABLE = "chatluna-studio.model-request";
3561
+ var MODEL_REQUEST_STATE_TABLE = "chatluna-studio.model-request-state";
3562
+ var MODEL_REQUEST_STATE_ID = "main";
3563
+ var DATABASE_READY_TIMEOUT_MS = 1e4;
3564
+ async function waitForDatabase(resolveDatabase, timeoutMs) {
3565
+ const deadline = Date.now() + timeoutMs;
3566
+ let database = resolveDatabase();
3567
+ while (!database && Date.now() < deadline) {
3568
+ await new Promise((resolve5) => setTimeout(resolve5, Math.min(25, Math.max(1, deadline - Date.now()))));
3569
+ database = resolveDatabase();
3570
+ }
3571
+ return database;
3572
+ }
3573
+ function registerStudioModelRequestModel(ctx) {
3574
+ ctx.model.extend(MODEL_REQUEST_RECORD_TABLE, {
3575
+ sequence: "unsigned",
3576
+ id: "string(64)",
3577
+ createdAt: "string(64)",
3578
+ attribution: "string(16)",
3579
+ botId: "string(64)",
3580
+ conversationId: "string(255)",
3581
+ interactionId: "string(64)",
3582
+ model: "string(255)",
3583
+ status: "string(32)",
3584
+ bytes: "unsigned",
3585
+ header: "json",
3586
+ bodies: "json"
3587
+ }, { primary: "sequence" });
3588
+ ctx.model.extend(MODEL_REQUEST_STATE_TABLE, {
3589
+ id: "string(32)",
3590
+ nextSequence: "unsigned",
3591
+ updatedAt: "timestamp"
3592
+ }, { primary: "id" });
3593
+ }
3594
+ function sequenceCursorCondition(order, beforeSequence) {
3595
+ return order === "asc" ? { $gt: beforeSequence } : { $lt: beforeSequence };
3596
+ }
3597
+ function fromModelRequestRow(row) {
3598
+ return structuredClone({ ...row.header, ...row.bodies ?? {} });
3599
+ }
3600
+ function isReadableModelRequestRow(row) {
3601
+ return Boolean(row.header && typeof row.header === "object" && typeof row.header.id === "string");
3602
+ }
3603
+ var KoishiDatabaseModelRequestPersistence = class {
3604
+ constructor(getDatabase, readyTimeoutMs = DATABASE_READY_TIMEOUT_MS) {
3605
+ this.getDatabase = getDatabase;
3606
+ this.readyTimeoutMs = readyTimeoutMs;
3607
+ }
3608
+ getDatabase;
3609
+ readyTimeoutMs;
3610
+ index = new ScopeRowIndex();
3611
+ async summarize() {
3612
+ const database = await waitForDatabase(this.getDatabase, this.readyTimeoutMs);
3613
+ if (!database) throw new Error(`\u7B49\u5F85 Koishi Database \u670D\u52A1 ${this.readyTimeoutMs}ms \u540E\u4ECD\u4E0D\u53EF\u7528`);
3614
+ const [state] = await database.get(MODEL_REQUEST_STATE_TABLE, { id: MODEL_REQUEST_STATE_ID });
3615
+ const rows = await database.get(MODEL_REQUEST_RECORD_TABLE, {}, {
3616
+ fields: ["sequence", "bytes"],
3617
+ sort: { sequence: "asc" }
3618
+ });
3619
+ this.index.reset(rows, Number(state?.nextSequence) || 1);
3620
+ return this.index.summary();
3621
+ }
3622
+ async append(record, bytes, nextSequence) {
3623
+ const database = this.requireDatabase();
3624
+ await database.upsert(MODEL_REQUEST_RECORD_TABLE, [this.toRow(record, bytes)]);
3625
+ this.index.put(record.sequence, bytes, nextSequence);
3626
+ await this.saveState(database);
3627
+ return this.index.summary();
3628
+ }
3629
+ async find(recordId) {
3630
+ const database = this.requireDatabase();
3631
+ const [row] = await database.get(MODEL_REQUEST_RECORD_TABLE, { id: recordId }, { limit: 1 });
3632
+ return row && isReadableModelRequestRow(row) ? fromModelRequestRow(row) : void 0;
3633
+ }
3634
+ async replace(record, bytes) {
3635
+ const database = this.requireDatabase();
3636
+ if (this.index.has(record.sequence)) {
3637
+ await database.upsert(MODEL_REQUEST_RECORD_TABLE, [this.toRow(record, bytes)]);
3638
+ this.index.put(record.sequence, bytes);
3639
+ }
3640
+ return this.index.summary();
3641
+ }
3642
+ async query(query) {
3643
+ const database = this.requireDatabase();
3644
+ const rows = await database.get(MODEL_REQUEST_RECORD_TABLE, this.toRowQuery(query), {
3645
+ sort: { sequence: query.order },
3646
+ limit: Math.max(0, query.limit)
3647
+ });
3648
+ return rows.filter(isReadableModelRequestRow).map((row) => fromModelRequestRow(row));
3649
+ }
3650
+ async queryHeaders(query) {
3651
+ const database = this.requireDatabase();
3652
+ const rows = await database.get(MODEL_REQUEST_RECORD_TABLE, this.toRowQuery(query), {
3653
+ fields: ["sequence", "header"],
3654
+ sort: { sequence: query.order },
3655
+ limit: Math.max(0, query.limit)
3656
+ });
3657
+ return rows.filter(isReadableModelRequestRow).map((row) => structuredClone(row.header));
3658
+ }
3659
+ async reclaim(limits) {
3660
+ const database = this.requireDatabase();
3661
+ const cutoff = this.index.resolveCutoff(limits);
3662
+ if (cutoff !== void 0) {
3663
+ await database.remove(MODEL_REQUEST_RECORD_TABLE, { sequence: { $lte: cutoff } });
3664
+ this.index.dropThrough(cutoff);
3665
+ }
3666
+ return this.index.summary();
3667
+ }
3668
+ async clear(nextSequence) {
3669
+ const database = this.requireDatabase();
3670
+ await database.remove(MODEL_REQUEST_RECORD_TABLE, {});
3671
+ this.index.clear(nextSequence);
3672
+ await this.saveState(database);
3673
+ return this.index.summary();
3674
+ }
3675
+ toRow(record, bytes) {
3676
+ return {
3677
+ sequence: record.sequence,
3678
+ id: record.id,
3679
+ createdAt: record.createdAt,
3680
+ attribution: record.attribution,
3681
+ // 缺省值写空串而不是 undefined:Minato 会丢弃 undefined 字段,过滤条件也就无法判定"未设置"。
3682
+ botId: record.entities.botId ?? "",
3683
+ conversationId: record.entities.conversationId ?? "",
3684
+ interactionId: record.interactionId ?? "",
3685
+ model: record.model ?? "",
3686
+ status: record.status,
3687
+ bytes,
3688
+ header: structuredClone(toModelRequestRecordHeader(record)),
3689
+ bodies: structuredClone({
3690
+ ...record.requestBody !== void 0 ? { requestBody: record.requestBody } : {},
3691
+ ...record.responseBodyRaw !== void 0 ? { responseBodyRaw: record.responseBodyRaw } : {}
3692
+ })
3693
+ };
3694
+ }
3695
+ toRowQuery(query) {
3696
+ const rowQuery = {};
3697
+ if (query.attribution) rowQuery.attribution = query.attribution;
3698
+ if (query.botId) rowQuery.botId = query.botId;
3699
+ if (query.conversationId) rowQuery.conversationId = query.conversationId;
3700
+ if (query.interactionId) rowQuery.interactionId = query.interactionId;
3701
+ if (query.model) rowQuery.model = query.model;
3702
+ if (query.errorsOnly) rowQuery.status = "error";
3703
+ if (query.beforeSequence !== void 0) {
3704
+ rowQuery.sequence = sequenceCursorCondition(query.order, query.beforeSequence);
3705
+ } else if (query.beforeCreatedAt) {
3706
+ const strict = query.order === "asc" ? { $gt: query.beforeCreatedAt } : { $lt: query.beforeCreatedAt };
3707
+ if (query.beforeId) {
3708
+ rowQuery.$or = [
3709
+ { createdAt: strict },
3710
+ { createdAt: query.beforeCreatedAt, id: query.order === "asc" ? { $gt: query.beforeId } : { $lt: query.beforeId } }
3711
+ ];
3712
+ } else {
3713
+ rowQuery.createdAt = query.order === "asc" ? { $gte: query.beforeCreatedAt } : { $lte: query.beforeCreatedAt };
3714
+ }
3715
+ }
3716
+ return rowQuery;
3717
+ }
3718
+ async saveState(database) {
3719
+ await database.upsert(MODEL_REQUEST_STATE_TABLE, [{
3720
+ id: MODEL_REQUEST_STATE_ID,
3721
+ nextSequence: this.index.readNextSequence(),
3722
+ updatedAt: /* @__PURE__ */ new Date()
3723
+ }]);
3724
+ }
3725
+ requireDatabase() {
3726
+ const database = this.getDatabase();
3727
+ if (!database) throw new Error("Koishi Database \u670D\u52A1\u4E0D\u53EF\u7528\uFF0C\u6A21\u578B\u8BF7\u6C42\u8BB0\u5F55\u672A\u80FD\u843D\u76D8");
3728
+ return database;
3729
+ }
3730
+ };
3731
+
3732
+ // src/presets/repository.ts
3733
+ var import_node_crypto = require("crypto");
3734
+ var import_promises = require("fs/promises");
3735
+ var import_node_path3 = require("path");
3736
+ var PresetRepositoryError = class extends Error {
3737
+ constructor(code, message, cause) {
3738
+ super(message);
3739
+ this.code = code;
3740
+ this.cause = cause;
3741
+ }
3742
+ code;
3743
+ cause;
3744
+ name = "PresetRepositoryError";
3745
+ };
3746
+ var mutationQueues = /* @__PURE__ */ new Map();
3747
+ var FileSystemPresetRepository = class {
3748
+ constructor(roots, options = {}) {
3749
+ this.options = options;
3750
+ this.rootPaths = {
3751
+ core: (0, import_node_path3.resolve)(roots.coreRoot),
3752
+ character: (0, import_node_path3.resolve)(roots.characterRoot)
3753
+ };
3754
+ }
3755
+ options;
3756
+ rootPaths;
3757
+ roots;
3758
+ async readAll(kind) {
3759
+ const root = await this.requireRoot(kind);
3760
+ const entries = await (0, import_promises.readdir)(root.realPath, { withFileTypes: true });
3761
+ const files = [];
3762
+ for (const entry of entries) {
3763
+ if (!entry.name.endsWith(".yml") || !entry.isFile()) continue;
3764
+ try {
3765
+ files.push(await this.readPresetFile(root, kind, entry.name));
3766
+ } catch (error) {
3767
+ if (error instanceof PresetRepositoryError && error.code === "unsafe-file") continue;
3768
+ throw error;
3769
+ }
3770
+ }
3771
+ return files.sort((left, right) => left.fileName.localeCompare(right.fileName));
3772
+ }
3773
+ async read(kind, fileName) {
3774
+ return this.readPresetFile(await this.requireRoot(kind), kind, fileName);
3775
+ }
3776
+ async create(input) {
3777
+ const root = await this.requireRoot(input.kind);
3778
+ const path = this.filePath(root, input.fileName);
3779
+ return withMutationLocks([path], async () => {
3780
+ await this.assertDestinationMissing(path);
3781
+ await this.atomicWrite(path, input.source, true, async () => {
3782
+ await this.beforeMutationCommit("create", void 0, path);
3783
+ await this.revalidateRoot(root);
3784
+ await this.assertDestinationMissing(path);
3785
+ });
3786
+ const snapshot = await this.readRegularFileSnapshot(path);
3787
+ return this.toPresetFile(input.kind, input.fileName, snapshot.source, snapshot.modifiedAt);
3788
+ });
3789
+ }
3790
+ async save(input) {
3791
+ const root = await this.requireRoot(input.kind);
3792
+ const path = this.filePath(root, input.fileName);
3793
+ return withMutationLocks([path], async () => {
3794
+ const original = await this.readRegularFileAfterRevision(path, input.expectedRevision);
3795
+ if (original.revision === revisionOf(input.source)) {
3796
+ return this.toPresetFile(input.kind, input.fileName, original.source, original.modifiedAt);
3797
+ }
3798
+ await this.atomicWrite(path, input.source, false, async () => {
3799
+ await this.beforeMutationCommit("save", path, path);
3800
+ await this.revalidateRoot(root);
3801
+ await this.revalidateSnapshot(path, original, input.expectedRevision);
3802
+ });
3803
+ const saved = await this.readRegularFileSnapshot(path);
3804
+ return this.toPresetFile(input.kind, input.fileName, saved.source, saved.modifiedAt);
3805
+ });
3806
+ }
3807
+ async rename(input) {
3808
+ this.requireConfirmation(input.confirmed);
3809
+ const root = await this.requireRoot(input.kind);
3810
+ const sourcePath = this.filePath(root, input.fileName);
3811
+ const destinationPath = this.filePath(root, input.newFileName);
3812
+ if (sourcePath === destinationPath) {
3813
+ return withMutationLocks([sourcePath], async () => {
3814
+ const snapshot = await this.readRegularFileAfterRevision(sourcePath, input.expectedRevision);
3815
+ return this.toPresetFile(input.kind, input.fileName, snapshot.source, snapshot.modifiedAt);
3816
+ });
3817
+ }
3818
+ return withMutationLocks([sourcePath, destinationPath], async () => {
3819
+ const original = await this.readRegularFileAfterRevision(sourcePath, input.expectedRevision);
3820
+ await this.assertDestinationMissing(destinationPath);
3821
+ await this.beforeMutationCommit("rename", sourcePath, destinationPath);
3822
+ await this.revalidateRoot(root);
3823
+ await this.revalidateSnapshot(sourcePath, original, input.expectedRevision);
3824
+ await this.assertDestinationMissing(destinationPath);
3825
+ let destinationIdentity;
3826
+ try {
3827
+ await (0, import_promises.link)(sourcePath, destinationPath);
3828
+ const destination = await this.readRegularFileSnapshot(destinationPath);
3829
+ destinationIdentity = destination.identity;
3830
+ this.assertSameSnapshot(destinationPath, destination, original, input.expectedRevision);
3831
+ await this.revalidateSnapshot(sourcePath, original, input.expectedRevision);
3832
+ await (0, import_promises.unlink)(sourcePath);
3833
+ } catch (error) {
3834
+ if (destinationIdentity) await removeIfIdentity(destinationPath, destinationIdentity);
3835
+ throw normalizeFileSystemError(error, input.fileName);
3836
+ }
3837
+ const renamed = await this.readRegularFileSnapshot(destinationPath);
3838
+ return this.toPresetFile(input.kind, input.newFileName, renamed.source, renamed.modifiedAt);
3839
+ });
3840
+ }
3841
+ async delete(input) {
3842
+ this.requireConfirmation(input.confirmed);
3843
+ const root = await this.requireRoot(input.kind);
3844
+ const path = this.filePath(root, input.fileName);
3845
+ await withMutationLocks([path], async () => {
3846
+ const original = await this.readRegularFileAfterRevision(path, input.expectedRevision);
3847
+ await this.beforeMutationCommit("delete", path);
3848
+ await this.revalidateRoot(root);
3849
+ await this.revalidateSnapshot(path, original, input.expectedRevision);
3850
+ try {
3851
+ await (0, import_promises.unlink)(path);
3852
+ } catch (error) {
3853
+ throw normalizeFileSystemError(error, input.fileName);
3854
+ }
3855
+ });
3856
+ }
3857
+ async initializeRoots() {
3858
+ const core = await this.initializeRoot(this.rootPaths.core);
3859
+ const character = await this.initializeRoot(this.rootPaths.character);
3860
+ return { core, character };
3861
+ }
3862
+ async initializeRoot(configuredPath) {
3863
+ await (0, import_promises.mkdir)(configuredPath, { recursive: true });
3864
+ const stats = await (0, import_promises.lstat)(configuredPath);
3865
+ if (!stats.isDirectory() || stats.isSymbolicLink()) throw new PresetRepositoryError("unsafe-root", `\u9884\u8BBE\u6839\u76EE\u5F55\u4E0D\u5B89\u5168\uFF1A${configuredPath}`);
3866
+ return { configuredPath, realPath: await (0, import_promises.realpath)(configuredPath) };
3867
+ }
3868
+ async requireRoot(kind) {
3869
+ const roots = await (this.roots ??= this.initializeRoots());
3870
+ const root = roots[kind];
3871
+ await this.revalidateRoot(root);
3872
+ return root;
3873
+ }
3874
+ async revalidateRoot(root) {
3875
+ let stats;
3876
+ let currentRealPath;
3877
+ try {
3878
+ stats = await (0, import_promises.lstat)(root.configuredPath);
3879
+ currentRealPath = await (0, import_promises.realpath)(root.configuredPath);
3880
+ } catch (error) {
3881
+ throw new PresetRepositoryError("unsafe-root", `\u9884\u8BBE\u6839\u76EE\u5F55\u4E0D\u53EF\u7528\uFF1A${root.configuredPath}`, error);
3882
+ }
3883
+ if (!stats.isDirectory() || stats.isSymbolicLink() || currentRealPath !== root.realPath) {
3884
+ throw new PresetRepositoryError("unsafe-root", `\u9884\u8BBE\u6839\u76EE\u5F55\u5DF2\u88AB\u66FF\u6362\uFF1A${root.configuredPath}`);
3885
+ }
3886
+ }
3887
+ filePath(root, fileName) {
3888
+ validateFileName(fileName);
3889
+ const path = (0, import_node_path3.join)(root.realPath, fileName);
3890
+ if ((0, import_node_path3.dirname)(path) !== root.realPath) throw new PresetRepositoryError("invalid-file-name", `\u65E0\u6548\u9884\u8BBE\u6587\u4EF6\u540D\uFF1A${fileName}`);
3891
+ return path;
3892
+ }
3893
+ async readPresetFile(root, kind, fileName) {
3894
+ const snapshot = await this.readRegularFileSnapshot(this.filePath(root, fileName));
3895
+ return this.toPresetFile(kind, fileName, snapshot.source, snapshot.modifiedAt);
3896
+ }
3897
+ async readRegularFileSnapshot(path) {
3898
+ let handle;
3899
+ try {
3900
+ const before = await (0, import_promises.lstat)(path, { bigint: true });
3901
+ if (before.isSymbolicLink() || !before.isFile()) throw new PresetRepositoryError("unsafe-file", `\u9884\u8BBE\u4E0D\u662F\u666E\u901A\u6587\u4EF6\uFF1A${(0, import_node_path3.basename)(path)}`);
3902
+ handle = await (0, import_promises.open)(path, import_promises.constants.O_RDONLY | noFollowFlag());
3903
+ const opened = await handle.stat({ bigint: true });
3904
+ if (!opened.isFile()) throw new PresetRepositoryError("unsafe-file", `\u9884\u8BBE\u4E0D\u662F\u666E\u901A\u6587\u4EF6\uFF1A${(0, import_node_path3.basename)(path)}`);
3905
+ const identity = identityOf(opened);
3906
+ if (!sameIdentity(identityOf(before), identity)) throw this.fileReplaced(path);
3907
+ const source2 = await handle.readFile({ encoding: "utf8" });
3908
+ const after = await (0, import_promises.lstat)(path, { bigint: true });
3909
+ if (after.isSymbolicLink() || !after.isFile() || !sameIdentity(identityOf(after), identity)) throw this.fileReplaced(path);
3910
+ return {
3911
+ source: source2,
3912
+ revision: revisionOf(source2),
3913
+ identity,
3914
+ modifiedAt: new Date(Number(opened.mtimeMs)).toISOString()
3915
+ };
3916
+ } catch (error) {
3917
+ throw normalizeFileSystemError(error, (0, import_node_path3.basename)(path));
3918
+ } finally {
3919
+ await handle?.close().catch(() => void 0);
3920
+ }
3921
+ }
3922
+ async readRegularFileAfterRevision(path, expectedRevision) {
3923
+ const snapshot = await this.readRegularFileSnapshot(path);
3924
+ if (snapshot.revision !== expectedRevision) throw this.revisionConflict((0, import_node_path3.basename)(path));
3925
+ return snapshot;
3926
+ }
3927
+ async revalidateSnapshot(path, original, expectedRevision) {
3928
+ const current = await this.readRegularFileSnapshot(path);
3929
+ this.assertSameSnapshot(path, current, original, expectedRevision);
3930
+ }
3931
+ assertSameSnapshot(path, current, original, expectedRevision) {
3932
+ if (!sameIdentity(current.identity, original.identity)) throw this.fileReplaced(path);
3933
+ if (current.revision !== expectedRevision) throw this.revisionConflict((0, import_node_path3.basename)(path));
3934
+ }
3935
+ async assertDestinationMissing(path) {
3936
+ try {
3937
+ await (0, import_promises.lstat)(path);
3938
+ } catch (error) {
3939
+ if (error.code === "ENOENT") return;
3940
+ throw normalizeFileSystemError(error, (0, import_node_path3.basename)(path));
3941
+ }
3942
+ throw new PresetRepositoryError("already-exists", `\u9884\u8BBE\u5DF2\u5B58\u5728\uFF1A${(0, import_node_path3.basename)(path)}`);
3943
+ }
3944
+ async atomicWrite(path, source2, createOnly, beforeCommit) {
3945
+ const temporaryPath = (0, import_node_path3.join)((0, import_node_path3.dirname)(path), `.${(0, import_node_path3.basename)(path)}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`);
3946
+ let handle;
3947
+ try {
3948
+ handle = await (0, import_promises.open)(temporaryPath, import_promises.constants.O_WRONLY | import_promises.constants.O_CREAT | import_promises.constants.O_EXCL | noFollowFlag(), 384);
3949
+ await handle.writeFile(source2, "utf8");
3950
+ await handle.sync();
3951
+ await handle.close();
3952
+ handle = void 0;
3953
+ await beforeCommit();
3954
+ if (createOnly) {
3955
+ await (0, import_promises.link)(temporaryPath, path);
3956
+ await (0, import_promises.unlink)(temporaryPath);
3957
+ } else {
3958
+ await (0, import_promises.rename)(temporaryPath, path);
3959
+ }
3960
+ } catch (error) {
3961
+ throw normalizeFileSystemError(error, (0, import_node_path3.basename)(path));
3962
+ } finally {
3963
+ await handle?.close().catch(() => void 0);
3964
+ await (0, import_promises.rm)(temporaryPath, { force: true }).catch(() => void 0);
3965
+ }
3966
+ }
3967
+ async beforeMutationCommit(operation, sourcePath, destinationPath) {
3968
+ await this.options.beforeMutationCommit?.({ operation, sourcePath, destinationPath });
3969
+ }
3970
+ toPresetFile(kind, fileName, source2, modifiedAt) {
3971
+ return {
3972
+ kind,
3973
+ fileName,
3974
+ source: source2,
3975
+ revision: revisionOf(source2),
3976
+ size: Buffer.byteLength(source2),
3977
+ modifiedAt,
3978
+ document: parsePresetSourceDocument(kind, source2)
3979
+ };
3980
+ }
3981
+ requireConfirmation(confirmed) {
3982
+ if (!confirmed) throw new PresetRepositoryError("confirmation-required", "\u91CD\u547D\u540D\u6216\u5220\u9664\u9884\u8BBE\u9700\u8981\u663E\u5F0F\u786E\u8BA4");
3983
+ }
3984
+ revisionConflict(fileName) {
3985
+ return new PresetRepositoryError("revision-conflict", `\u9884\u8BBE\u5DF2\u88AB\u5176\u4ED6\u64CD\u4F5C\u4FEE\u6539\uFF1A${fileName}`);
3986
+ }
3987
+ fileReplaced(path) {
3988
+ return new PresetRepositoryError("unsafe-file", `\u9884\u8BBE\u6587\u4EF6\u5DF2\u88AB\u66FF\u6362\uFF1A${(0, import_node_path3.basename)(path)}`);
3989
+ }
3990
+ };
3991
+ async function withMutationLocks(paths, action) {
3992
+ const releases = [];
3993
+ try {
3994
+ for (const path of [...new Set(paths)].sort()) releases.push(await acquireMutationLock(path));
3995
+ return await action();
3996
+ } finally {
3997
+ for (const release of releases.reverse()) release();
3998
+ }
3999
+ }
4000
+ async function acquireMutationLock(path) {
4001
+ const previous = mutationQueues.get(path) ?? Promise.resolve();
4002
+ let releaseGate;
4003
+ const gate = new Promise((resolve5) => {
4004
+ releaseGate = resolve5;
4005
+ });
4006
+ const tail = previous.catch(() => void 0).then(() => gate);
4007
+ mutationQueues.set(path, tail);
4008
+ await previous.catch(() => void 0);
4009
+ return () => {
4010
+ if (mutationQueues.get(path) === tail) mutationQueues.delete(path);
4011
+ releaseGate();
4012
+ };
4013
+ }
4014
+ async function removeIfIdentity(path, identity) {
4015
+ try {
4016
+ const stats = await (0, import_promises.lstat)(path, { bigint: true });
4017
+ if (!stats.isSymbolicLink() && stats.isFile() && sameIdentity(identityOf(stats), identity)) await (0, import_promises.unlink)(path);
4018
+ } catch {
4019
+ }
4020
+ }
4021
+ function identityOf(stats) {
4022
+ return { dev: stats.dev, ino: stats.ino };
4023
+ }
4024
+ function sameIdentity(left, right) {
4025
+ return left.dev === right.dev && left.ino === right.ino;
4026
+ }
4027
+ function validateFileName(fileName) {
4028
+ if (!fileName || fileName !== (0, import_node_path3.basename)(fileName) || (0, import_node_path3.isAbsolute)(fileName) || (0, import_node_path3.normalize)(fileName) !== fileName || fileName.includes("/") || fileName.includes("\\") || !fileName.endsWith(".yml") || fileName === ".yml" || fileName.includes("\0")) {
4029
+ throw new PresetRepositoryError("invalid-file-name", `\u65E0\u6548\u9884\u8BBE\u6587\u4EF6\u540D\uFF1A${fileName}`);
4030
+ }
4031
+ }
4032
+ function revisionOf(source2) {
4033
+ return (0, import_node_crypto.createHash)("sha256").update(source2, "utf8").digest("hex");
4034
+ }
4035
+ function noFollowFlag() {
4036
+ return typeof import_promises.constants.O_NOFOLLOW === "number" ? import_promises.constants.O_NOFOLLOW : 0;
4037
+ }
4038
+ function normalizeFileSystemError(error, fileName) {
4039
+ if (error instanceof PresetRepositoryError) return error;
4040
+ const code = error.code;
4041
+ if (code === "ENOENT") return new PresetRepositoryError("not-found", `\u9884\u8BBE\u4E0D\u5B58\u5728\uFF1A${fileName}`, error);
4042
+ if (code === "EEXIST") return new PresetRepositoryError("already-exists", `\u9884\u8BBE\u5DF2\u5B58\u5728\uFF1A${fileName}`, error);
4043
+ if (code === "ELOOP") return new PresetRepositoryError("unsafe-file", `\u62D2\u7EDD\u8BBF\u95EE symlink \u9884\u8BBE\uFF1A${fileName}`, error);
4044
+ return new PresetRepositoryError("unsafe-file", `\u65E0\u6CD5\u5B89\u5168\u8BBF\u95EE\u9884\u8BBE\uFF1A${fileName}`, error);
4045
+ }
4046
+
4047
+ // src/presets/runtime-correlation.ts
4048
+ var PresetRuntimeSnapshotTracker = class {
4049
+ constructor(ctx) {
4050
+ this.ctx = ctx;
4051
+ const on = ctx.on.bind(ctx);
4052
+ this.disposers.push(on("chatluna/before-chat", (conversationId, _message, variables, chatInterface, session) => {
4053
+ const target = this.resolveSession(session);
4054
+ const presetName = readNestedString(variables, ["built", "preset"]);
4055
+ if (!target || !presetName) return;
4056
+ const preset = resolveCorePreset(chatInterface, this.ctx, presetName);
4057
+ if (!preset) return;
4058
+ const snapshot = snapshotCorePreset(presetName, preset);
4059
+ if (!snapshot) return;
4060
+ const turnId = `core:${conversationId}`;
4061
+ const key = activeKey(target, `${turnId}:${this.nextCoreTurn++}`);
4062
+ this.active.set(key, { target, turnId, snapshot });
4063
+ const keys = this.coreTurnKeys.get(conversationId) ?? /* @__PURE__ */ new Set();
4064
+ keys.add(key);
4065
+ this.coreTurnKeys.set(conversationId, keys);
4066
+ }));
4067
+ this.disposers.push(on("chatluna/after-chat", (conversationId, _sourceMessage, _responseMessage, _variables, _chatInterface, session) => {
4068
+ this.finishCore(conversationId, this.resolveSession(session));
4069
+ }));
4070
+ this.disposers.push(on("chatluna/after-chat-error", (_error, conversationId) => this.finishCore(conversationId)));
4071
+ this.disposers.push(on("chatluna_character/before-chat", (payload) => {
4072
+ const record = readRecord3(payload);
4073
+ const target = this.resolveSession(record?.session);
4074
+ const preset = readRecord3(record?.preset);
4075
+ const presetName = readString2(record?.presetName) ?? readString2(preset?.name);
4076
+ if (!target || !preset || !presetName) return;
4077
+ const snapshot = snapshotCharacterPreset(presetName, preset);
4078
+ if (!snapshot) return;
4079
+ const turnId = `character:${this.nextCharacterTurn++}`;
4080
+ const key = activeKey(target, turnId);
4081
+ this.active.set(key, { target, turnId, snapshot });
4082
+ const session = record?.session;
4083
+ if (session && typeof session === "object") {
4084
+ const keys = this.characterSessionTurnKeys.get(session) ?? /* @__PURE__ */ new Set();
4085
+ keys.add(key);
4086
+ this.characterSessionTurnKeys.set(session, keys);
4087
+ }
4088
+ }));
4089
+ this.disposers.push(on("chatluna_character/after-chat", (payload) => {
4090
+ const record = readRecord3(payload);
4091
+ const target = this.resolveSession(record?.session);
4092
+ if (!target) return;
4093
+ const session = record?.session;
4094
+ const sessionKeys = session && typeof session === "object" ? this.characterSessionTurnKeys.get(session) : void 0;
4095
+ if (session && typeof session === "object" && sessionKeys?.size === 1) {
4096
+ const [directKey] = sessionKeys;
4097
+ if (directKey) this.active.delete(directKey);
4098
+ this.characterSessionTurnKeys.delete(session);
4099
+ return;
4100
+ }
4101
+ const matches = [...this.active].filter(([, turn]) => turn.snapshot.kind === "character" && sameTarget(turn.target, target));
4102
+ if (matches.length === 1) this.active.delete(matches[0][0]);
4103
+ }));
4104
+ }
4105
+ ctx;
4106
+ active = /* @__PURE__ */ new Map();
4107
+ coreTurnKeys = /* @__PURE__ */ new Map();
4108
+ characterSessionTurnKeys = /* @__PURE__ */ new WeakMap();
4109
+ disposers = [];
4110
+ nextCoreTurn = 1;
4111
+ nextCharacterTurn = 1;
4112
+ getActiveSnapshots(target) {
4113
+ const turns = [...this.active.values()].filter((turn) => sameTarget(turn.target, target));
4114
+ const snapshots = ["core", "character"].flatMap((kind) => {
4115
+ const matchingKind = turns.filter((turn) => turn.snapshot.kind === kind);
4116
+ return matchingKind.length === 1 ? [matchingKind[0].snapshot] : [];
4117
+ });
4118
+ return structuredClone(snapshots);
4119
+ }
4120
+ dispose() {
4121
+ for (const dispose of this.disposers.splice(0)) dispose();
4122
+ this.active.clear();
4123
+ this.coreTurnKeys.clear();
4124
+ }
4125
+ resolveSession(session) {
4126
+ const record = readRecord3(session);
4127
+ const bot = readRecord3(record?.bot);
4128
+ const channel = readRecord3(record?.channel);
4129
+ const botId = readString2(record?.selfId) ?? readString2(bot?.selfId);
4130
+ const conversationId = readString2(record?.channelId) ?? readString2(channel?.id);
4131
+ return botId && conversationId ? { botId, conversationId } : void 0;
4132
+ }
4133
+ finishCore(conversationId, target) {
4134
+ const keys = this.coreTurnKeys.get(conversationId);
4135
+ if (!keys) return;
4136
+ const matches = [...keys].filter((key) => {
4137
+ const turn = this.active.get(key);
4138
+ return turn && (!target || sameTarget(turn.target, target));
4139
+ });
4140
+ if (matches.length !== 1) return;
4141
+ this.active.delete(matches[0]);
4142
+ keys.delete(matches[0]);
4143
+ if (!keys.size) this.coreTurnKeys.delete(conversationId);
4144
+ }
4145
+ };
4146
+ function snapshotCorePreset(presetName, preset) {
4147
+ const templates = coreTemplates(preset);
4148
+ if (!templates.length) return;
4149
+ return structuredClone({
4150
+ kind: "core",
4151
+ presetName,
4152
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
4153
+ ...typeof preset.rawText === "string" ? { source: preset.rawText } : {},
4154
+ templates
4155
+ });
4156
+ }
4157
+ function coreTemplates(preset) {
4158
+ if (typeof preset.rawText === "string") {
4159
+ const document = parsePresetSourceDocument("core", preset.rawText);
4160
+ if (!document.diagnostics.length) {
4161
+ const templates = document.templateFields.flatMap((field) => {
4162
+ const role = coreRoleForPath(preset, field.path);
4163
+ return role ? [{ path: field.path, role, template: field.value }] : [];
4164
+ });
4165
+ if (typeof preset.formatUserPromptString === "string" && !templates.some(({ path }) => path[0] === "format_user_prompt")) {
4166
+ templates.push({ path: ["format_user_prompt"], role: "user", template: preset.formatUserPromptString });
4167
+ }
4168
+ return templates;
4169
+ }
4170
+ }
4171
+ const messages = Array.isArray(preset.messages) ? preset.messages : [];
4172
+ const result = messages.flatMap((message, index) => {
4173
+ const record = readRecord3(message);
4174
+ const content = readMessageContent(record?.content);
4175
+ const role = readMessageRole(message);
4176
+ return content !== void 0 && role ? [{ path: ["prompts", index, "content"], role, template: content }] : [];
4177
+ });
4178
+ if (typeof preset.formatUserPromptString === "string") {
4179
+ result.push({ path: ["format_user_prompt"], role: "user", template: preset.formatUserPromptString });
4180
+ }
4181
+ return result;
4182
+ }
4183
+ function coreRoleForPath(preset, path) {
4184
+ if (path[0] === "format_user_prompt") return "user";
4185
+ const index = typeof path[1] === "number" ? path[1] : void 0;
4186
+ return index === void 0 ? void 0 : readMessageRole(Array.isArray(preset.messages) ? preset.messages[index] : void 0);
4187
+ }
4188
+ function readMessageRole(message) {
4189
+ const record = readRecord3(message);
4190
+ const direct = readString2(record?.role)?.toLowerCase();
4191
+ let type = direct;
4192
+ const getType = record?._getType;
4193
+ if (!type && typeof getType === "function") {
4194
+ try {
4195
+ type = readString2(getType.call(message))?.toLowerCase();
4196
+ } catch {
4197
+ }
4198
+ }
4199
+ if (type === "system" || type === "developer") return "system";
4200
+ if (type === "human" || type === "user") return "user";
4201
+ if (type === "ai" || type === "assistant") return "assistant";
4202
+ if (type === "tool" || type === "function") return "tool";
4203
+ }
4204
+ function readMessageContent(value) {
4205
+ if (typeof value === "string") return value;
4206
+ const record = readRecord3(value);
4207
+ return readString2(record?.text);
4208
+ }
4209
+ function snapshotCharacterPreset(presetName, preset) {
4210
+ const system = typeof preset.system?.rawString === "string" ? preset.system.rawString : void 0;
4211
+ const input = typeof preset.input?.rawString === "string" ? preset.input.rawString : void 0;
4212
+ if (system === void 0 && input === void 0) return;
4213
+ const templates = [];
4214
+ if (system !== void 0) templates.push({ path: ["system"], role: "system", template: system });
4215
+ if (input !== void 0) templates.push({ path: ["input"], role: "user", template: input });
4216
+ return structuredClone({
4217
+ kind: "character",
4218
+ presetName,
4219
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
4220
+ source: [system === void 0 ? void 0 : `system: ${system}`, input === void 0 ? void 0 : `input: ${input}`].filter(Boolean).join("\n"),
4221
+ templates
4222
+ });
4223
+ }
4224
+ function resolveCorePreset(chatInterface, ctx, presetName) {
4225
+ const interfaceContext = readRecord3(readRecord3(chatInterface)?.ctx);
4226
+ const candidates = [readRecord3(interfaceContext?.chatluna), readRecord3(readRecord3(ctx)?.chatluna)];
4227
+ for (const chatluna of candidates) {
4228
+ const service = readRecord3(chatluna?.preset);
4229
+ const getPreset = service?.getPreset;
4230
+ if (typeof getPreset !== "function") continue;
4231
+ try {
4232
+ const computed = getPreset.call(service, presetName, false);
4233
+ const value = readRecord3(readRecord3(computed)?.value);
4234
+ if (value) return value;
4235
+ } catch {
4236
+ }
4237
+ }
4238
+ }
4239
+ function activeKey(target, turnId) {
4240
+ return `${target.botId}\0${target.conversationId}\0${turnId}`;
4241
+ }
4242
+ function sameTarget(left, right) {
4243
+ return left.botId === right.botId && left.conversationId === right.conversationId;
4244
+ }
4245
+ function readRecord3(value) {
4246
+ return value !== null && typeof value === "object" ? value : void 0;
4247
+ }
4248
+ function readString2(value) {
4249
+ return typeof value === "string" && value ? value : void 0;
4250
+ }
4251
+ function readNestedString(value, path) {
4252
+ let current = value;
4253
+ for (const key of path) current = readRecord3(current)?.[key];
4254
+ return readString2(current);
4255
+ }
4256
+
4257
+ // src/presets/service.ts
4258
+ var import_node_path4 = require("path");
4259
+ var StudioPresetService = class {
4260
+ constructor(options) {
4261
+ this.options = options;
4262
+ this.repository = options.repository ?? new FileSystemPresetRepository({
4263
+ coreRoot: (0, import_node_path4.resolve)(options.baseDir, "data/chathub/presets"),
4264
+ characterRoot: (0, import_node_path4.resolve)(options.baseDir, "data/chathub/character/presets")
4265
+ });
4266
+ }
4267
+ options;
4268
+ repository;
4269
+ async catalog(kind) {
4270
+ const kinds = kind ? [kind] : ["core", "character"];
4271
+ const files = await Promise.all(kinds.map((documentKind) => this.repository.readAll(documentKind)));
4272
+ return files.flat().map((file) => this.present(file)).sort((left, right) => left.kind.localeCompare(right.kind) || left.displayName?.localeCompare(right.displayName ?? "") || left.fileName.localeCompare(right.fileName));
4273
+ }
4274
+ async read(input) {
4275
+ return this.present(await this.repository.read(input.kind, input.fileName));
4276
+ }
4277
+ async create(input) {
4278
+ return this.present(await this.repository.create(input));
4279
+ }
4280
+ async save(input) {
4281
+ return this.present(await this.repository.save(input));
4282
+ }
4283
+ async rename(input) {
4284
+ return this.present(await this.repository.rename(input));
4285
+ }
4286
+ async delete(input) {
4287
+ await this.repository.delete(input);
4288
+ return { deleted: true };
4289
+ }
4290
+ async locateExpression(input) {
4291
+ let document;
4292
+ try {
4293
+ document = await this.read(input.document);
4294
+ } catch (error) {
4295
+ if (error instanceof PresetRepositoryError && error.code === "not-found") {
4296
+ return failure("document-not-found", error.message);
4297
+ }
4298
+ throw error;
4299
+ }
4300
+ if (document.revision !== input.document.revision) {
4301
+ return failure("document-stale", "\u9884\u8BBE\u6E90\u7801 revision \u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5");
4302
+ }
4303
+ if (!document.displayName) {
4304
+ return failure("document-identity-unavailable", "\u9884\u8BBE\u7F3A\u5C11\u53EF\u7528\u4E8E\u8FD0\u884C\u65F6\u5173\u8054\u7684\u5C55\u793A\u8EAB\u4EFD");
4305
+ }
4306
+ const expression = resolveExpression(document.expressions, input.expression);
4307
+ if (!expression) return failure("expression-not-found", "\u9884\u8BBE\u8868\u8FBE\u5F0F\u4E0D\u5B58\u5728\u6216\u5750\u6807\u5DF2\u53D8\u5316");
4308
+ const recordAndSnapshot = await findLatestMatchingRequest(this.options.modelRequests, document);
4309
+ if (!recordAndSnapshot) {
4310
+ return failure("request-not-observed", "\u8BB0\u5F55\u5E93\u91CC\u6CA1\u6709\u4F7F\u7528\u8BE5\u9884\u8BBE\u5F53\u524D\u6A21\u677F\u7684\u6709\u5F52\u5C5E\u6210\u529F\u6A21\u578B\u8BF7\u6C42");
4311
+ }
4312
+ const { record, snapshot } = recordAndSnapshot;
4313
+ const result = matchPresetExpressionEvidence({
4314
+ document: {
4315
+ kind: document.kind,
4316
+ source: document.source,
4317
+ templateFields: document.templateFields,
4318
+ expressions: document.expressions,
4319
+ diagnostics: document.diagnostics
4320
+ },
4321
+ expression,
4322
+ snapshot,
4323
+ evidence: projectModelEvidence({
4324
+ requestBody: record.requestBody,
4325
+ responseBodyRaw: record.responseBodyRaw,
4326
+ responseBodyFormat: record.responseBodyFormat
4327
+ })
4328
+ });
4329
+ if (result.status === "matched") {
4330
+ return {
4331
+ status: "matched",
4332
+ recordId: record.id,
4333
+ evidenceId: result.evidenceId,
4334
+ range: result.range
4335
+ };
4336
+ }
4337
+ if (result.status === "stale") return failure("expression-stale", "\u8868\u8FBE\u5F0F\u6216\u8FD0\u884C\u65F6\u6A21\u677F\u5DF2\u53D8\u5316");
4338
+ if (result.status === "not-observed") return failure("expression-not-observed", "\u6A21\u578B\u8BF7\u6C42\u8BC1\u636E\u4E2D\u672A\u89C2\u5BDF\u5230\u8BE5\u8868\u8FBE\u5F0F\u7684\u5C55\u5F00\u8303\u56F4");
4339
+ if (result.status === "ambiguous") return failure("expression-ambiguous", "\u6A21\u578B\u8BF7\u6C42\u8BC1\u636E\u4E2D\u5B58\u5728\u591A\u4E2A\u53EF\u80FD\u8303\u56F4\uFF0C\u65E0\u6CD5\u552F\u4E00\u5B9A\u4F4D");
4340
+ return {
4341
+ ...failure("expression-unsupported", "\u8BE5\u8868\u8FBE\u5F0F\u4E0D\u80FD\u6620\u5C04\u4E3A\u7CBE\u786E\u6A21\u578B\u8BC1\u636E\u8303\u56F4"),
4342
+ reason: result.reason
4343
+ };
4344
+ }
4345
+ present(file) {
4346
+ return {
4347
+ kind: file.kind,
4348
+ fileName: file.fileName,
4349
+ displayName: file.document.displayName,
4350
+ source: file.source,
4351
+ revision: file.revision,
4352
+ size: file.size,
4353
+ modifiedAt: file.modifiedAt,
4354
+ templateFields: structuredClone(file.document.templateFields),
4355
+ expressions: file.document.expressions.map((expression) => ({
4356
+ ...structuredClone(expression),
4357
+ stableId: expressionStableId(expression)
4358
+ })),
4359
+ diagnostics: structuredClone(file.document.diagnostics)
4360
+ };
4361
+ }
4362
+ };
4363
+ function expressionStableId(expression) {
4364
+ return `${JSON.stringify(expression.path)}#${expression.occurrence}`;
4365
+ }
4366
+ function resolveExpression(expressions, identity) {
4367
+ if ("stableId" in identity) return expressions.find(({ stableId }) => stableId === identity.stableId);
4368
+ return expressions.find((expression) => expression.occurrence === identity.occurrence && samePath2(expression.path, identity.path) && expression.range.start === identity.range.start && expression.range.end === identity.range.end);
4369
+ }
4370
+ async function findLatestMatchingRequest(store, document) {
4371
+ let beforeSequence;
4372
+ do {
4373
+ const page = await store.getRecords({ attribution: "attributed", order: "desc", limit: 200, beforeSequence });
4374
+ for (const item of page.records) {
4375
+ if (item.status !== "success" || !item.requestBodyAvailable) continue;
4376
+ if (!item.presetSnapshotSummaries?.some((snapshot2) => snapshot2.kind === document.kind && snapshot2.presetName === document.displayName)) continue;
4377
+ const record = await store.getRecord(item.id);
4378
+ if (!record || record.requestBody === void 0) continue;
4379
+ const snapshot = record.presetSnapshots?.find((candidate) => snapshotMatchesDocument(candidate, document));
4380
+ if (snapshot) return { record, snapshot };
4381
+ }
4382
+ beforeSequence = page.nextCursor;
4383
+ if (!page.hasMore) return;
4384
+ } while (beforeSequence !== void 0);
4385
+ }
4386
+ function snapshotMatchesDocument(snapshot, document) {
4387
+ if (snapshot.kind !== document.kind || snapshot.presetName !== document.displayName) return false;
4388
+ if (snapshot.source === document.source) return true;
4389
+ if (snapshot.templates.length !== document.templateFields.length) return false;
4390
+ return document.templateFields.every((field) => snapshot.templates.some((template) => samePath2(template.path, field.path) && template.template === field.value));
4391
+ }
4392
+ function samePath2(left, right) {
4393
+ return left.length === right.length && left.every((part, index) => part === right[index]);
4394
+ }
4395
+ function failure(code, message) {
4396
+ return { status: "failed", code, message };
4397
+ }
4398
+
4399
+ // src/index.ts
4400
+ var name = "chatluna-studio";
4401
+ var usage = `
4402
+ \u5728 Koishi \u63A7\u5236\u53F0\u91CC\u67E5\u770B ChatLuna \u7684\u771F\u5B9E\u6A21\u578B\u8BF7\u6C42\uFF0C\u5E76\u76F4\u63A5\u7F16\u8F91 ChatLuna \u4E0E chatluna-character \u7684\u9884\u8BBE\u6587\u4EF6\u3002
4403
+
4404
+ - \u6A21\u578B\u8BF7\u6C42\u9875\u9762\u8BB0\u5F55\u6BCF\u6B21\u771F\u5B9E\u5BF9\u8BDD\u89E6\u53D1\u7684\u6A21\u578B\u8C03\u7528\uFF1A\u8BF7\u6C42\u4F53\u3001\u54CD\u5E94\u539F\u6587\u3001\u7528\u91CF\u3001\u8F68\u8FF9\u4E0E\u9884\u8BBE\u53D8\u91CF\u5C55\u5F00\u3002
4405
+ - \u9884\u8BBE\u9875\u9762\u76F4\u63A5\u8BFB\u5199\`data/chathub/presets\`\u4E0E\`data/chathub/character/presets\`\u4E0B\u7684 YAML \u6587\u4EF6\uFF0C\u4FDD\u5B58\u540E\u7531 ChatLuna \u81EA\u5DF1\u70ED\u91CD\u8F7D\u3002
4406
+ - \u70B9\u51FB\u9884\u8BBE\u91CC\u7684\u8868\u8FBE\u5F0F\u53EF\u4EE5\u8DF3\u5230\u5B83\u5728\u771F\u5B9E\u6A21\u578B\u8BF7\u6C42\u4E2D\u7684\u5C55\u5F00\u4F4D\u7F6E\uFF0C\u9700\u8981\u5148\u6709\u4E00\u6B21\u7528\u5230\u8BE5\u9884\u8BBE\u7684\u6210\u529F\u8BF7\u6C42\u3002
4407
+
4408
+ \u6A21\u578B\u8BF7\u6C42\u7684\u91C7\u96C6\u5728 ChatLuna \u7684 fetch \u8FB9\u754C\u5B8C\u6210\uFF0C\u56E0\u6B64\u9700\u8981\u4E0E ChatLuna \u88C5\u5728\u540C\u4E00\u4E2A Koishi \u5B9E\u4F8B\u91CC\u3002
4409
+ `;
4410
+ var inject = {
4411
+ required: ["console"],
4412
+ optional: ["database"]
4413
+ };
4414
+ var Config = import_koishi3.Schema.object({
4415
+ persistenceMode: import_koishi3.Schema.union([
4416
+ import_koishi3.Schema.const("memory").description("\u670D\u52A1\u7AEF\u5185\u5B58"),
4417
+ import_koishi3.Schema.const("database").description("Koishi Database")
4418
+ ]).default("database").role("radio").description("\u6A21\u578B\u8BF7\u6C42\u8BB0\u5F55\u7684\u5B58\u50A8\u65B9\u5F0F\u3002\u5185\u5B58\u6A21\u5F0F\u91CD\u542F\u540E\u8BB0\u5F55\u6E05\u7A7A"),
4419
+ modelRequestRecordLimit: import_koishi3.Schema.number().min(1).default(DEFAULT_MODEL_REQUEST_RECORD_LIMIT).description("\u4FDD\u7559\u7684\u6A21\u578B\u8BF7\u6C42\u8BB0\u5F55\u6761\u6570\u4E0A\u9650\uFF0C\u8D85\u51FA\u540E\u4ECE\u6700\u65E7\u8BB0\u5F55\u5F00\u59CB\u4E22\u5F03"),
4420
+ studioColorMode: import_koishi3.Schema.union([
4421
+ import_koishi3.Schema.const("auto").description("\u81EA\u52A8"),
4422
+ import_koishi3.Schema.const("light").description("\u660E\u4EAE"),
4423
+ import_koishi3.Schema.const("dark").description("\u6697\u8272")
4424
+ ]).default("auto").role("radio").description("\u9875\u9762\u989C\u8272\u6A21\u5F0F\u3002\u81EA\u52A8\u65F6\u8DDF\u968F Koishi \u63A7\u5236\u53F0"),
4425
+ studioAccentColor: import_koishi3.Schema.string().default("#2563eb").role("color").description("\u9875\u9762\u5F3A\u8C03\u8272"),
4426
+ enableStudioFrostedGlass: import_koishi3.Schema.boolean().default(true).description("\u542F\u7528\u6D6E\u5C42\u6BDB\u73BB\u7483\u6548\u679C"),
4427
+ studioUseQQAvatars: import_koishi3.Schema.boolean().default(true).description("\u4ECE QQ \u5934\u50CF CDN \u52A0\u8F7D\u673A\u5668\u4EBA\u4E0E\u4F1A\u8BDD\u5934\u50CF\u3002\u5173\u95ED\u540E\u53EA\u663E\u793A\u9996\u5B57\u6BCD\u8272\u5757\uFF0C\u6D4F\u89C8\u5668\u4E0D\u4F1A\u5411`q.qlogo.cn`\u53D1\u8D77\u8BF7\u6C42")
4428
+ }).description("ChatLuna Studio");
4429
+ function findChatLunaUsage(ctx) {
4430
+ for (const runtime of ctx.registry.values()) {
4431
+ const service = runtime.ctx.get("console.services.chatluna_usage");
4432
+ if (service) return service;
4433
+ }
4434
+ }
4435
+ function apply(ctx, config) {
4436
+ ctx.inject({
4437
+ console: { required: true },
4438
+ database: { required: false }
4439
+ }, (inner) => {
4440
+ const logger = inner.logger("chatluna-studio");
4441
+ let persistence;
4442
+ if (config.persistenceMode === "database") {
4443
+ registerStudioModelRequestModel(inner);
4444
+ persistence = new KoishiDatabaseModelRequestPersistence(() => inner.database);
4445
+ }
4446
+ const modelRequests = new StudioModelRequestStore({
4447
+ ...persistence ? { persistence } : {},
4448
+ maxRecords: config.modelRequestRecordLimit
4449
+ });
4450
+ const presetSnapshots = new PresetRuntimeSnapshotTracker(inner);
4451
+ const sessions = new ChatLunaSessionTracker(inner, {
4452
+ // ChatLuna 规范错误只出现在对话链路的错误事件里,而模型请求记录只看得到 HTTP 层的失败;
4453
+ // 把它回填到本轮最近一条失败记录上,详情页才能给出错误码与可能原因。
4454
+ onTurnFailed: (error, turn) => {
4455
+ const conversationId = turn.entities.conversationId;
4456
+ if (conversationId) archiveChatLunaModelRequestError(modelRequests, error, { conversationId });
4457
+ }
4458
+ });
4459
+ const presets = new StudioPresetService({ baseDir: inner.baseDir, modelRequests });
4460
+ const getPersistenceStatus = () => {
4461
+ if (config.persistenceMode !== "database") {
4462
+ return { mode: "memory", available: true, persisted: false };
4463
+ }
4464
+ const available = Boolean(inner.database);
4465
+ return {
4466
+ mode: "database",
4467
+ available,
4468
+ persisted: available,
4469
+ ...available ? {} : { message: "Koishi Database \u670D\u52A1\u672A\u5B89\u88C5\u6216\u4E0D\u53EF\u7528\uFF0C\u6A21\u578B\u8BF7\u6C42\u8BB0\u5F55\u4E0D\u4F1A\u843D\u76D8" }
4470
+ };
4471
+ };
4472
+ const notifyRecorded = registerConsole(inner.console, {
4473
+ modelRequests,
4474
+ presets,
4475
+ appearance: {
4476
+ enableStudioFrostedGlass: config.enableStudioFrostedGlass,
4477
+ studioColorMode: config.studioColorMode,
4478
+ studioAccentColor: config.studioAccentColor,
4479
+ studioUseQQAvatars: config.studioUseQQAvatars
4480
+ },
4481
+ getPersistenceStatus,
4482
+ // chatluna-usage 位于另一个 loader group,Cordis 会为服务建立隔离映射;
4483
+ // 复用 usage 插件的 Context 才能解析到同一实例。
4484
+ chatlunaUsage: () => findChatLunaUsage(inner)
4485
+ });
4486
+ const chatLunaPlugin = resolveChatLunaPluginClass(inner.baseDir);
4487
+ const disposeCollector = installModelRequestCollector({
4488
+ ...chatLunaPlugin ? { plugin: chatLunaPlugin } : {},
4489
+ baseDir: inner.baseDir,
4490
+ store: modelRequests,
4491
+ resolveTurn: () => sessions.resolveActiveTurn(),
4492
+ getActivePresetSnapshots: (target) => presetSnapshots.getActiveSnapshots(target),
4493
+ onRecordAppended: ({ sequence }) => notifyRecorded(sequence)
4494
+ });
4495
+ logger.info(chatLunaPlugin ? "ChatLuna \u6A21\u578B\u8BF7\u6C42\u91C7\u96C6\u5668\u5DF2\u5B89\u88C5\u3002" : "\u672A\u627E\u5230 ChatLuna \u8FD0\u884C\u65F6\uFF0C\u6A21\u578B\u8BF7\u6C42\u91C7\u96C6\u5668\u672A\u5B89\u88C5\u3002");
4496
+ inner.on("chatluna/model-usage", (payload) => {
4497
+ void linkChatLunaUsageRequest([modelRequests], payload).catch((error) => {
4498
+ logger.warn("ChatLuna \u7528\u91CF\u4E8B\u4EF6\u5173\u8054\u6A21\u578B\u8BF7\u6C42\u5931\u8D25\u3002", error);
4499
+ });
4500
+ });
4501
+ inner.on("dispose", () => {
4502
+ disposeCollector();
4503
+ presetSnapshots.dispose();
4504
+ sessions.dispose();
4505
+ void modelRequests.waitForPersistence().catch((error) => {
4506
+ logger.error("\u6A21\u578B\u8BF7\u6C42\u8BB0\u5F55\u5173\u673A\u6536\u5C3E\u6301\u4E45\u5316\u5931\u8D25\u3002", error);
4507
+ });
4508
+ });
4509
+ });
4510
+ }
4511
+ // Annotate the CommonJS export names for ESM import in node:
4512
+ 0 && (module.exports = {
4513
+ CHATLUNA_ERROR_CODE_DOCUMENTATION_URL,
4514
+ ChatLunaSessionTracker,
4515
+ Config,
4516
+ DEFAULT_MODEL_REQUEST_PAGE_SIZE,
4517
+ DEFAULT_MODEL_REQUEST_RECORD_LIMIT,
4518
+ DEFAULT_MODEL_REQUEST_RECORD_MAX_BYTES,
4519
+ FileSystemPresetRepository,
4520
+ InMemoryModelRequestRecords,
4521
+ InMemoryRecordRows,
4522
+ KoishiDatabaseModelRequestPersistence,
4523
+ MAX_MODEL_REQUEST_PAGE_SIZE,
4524
+ MODEL_REQUEST_FACET_SCAN_LIMIT,
4525
+ PresetRepositoryError,
4526
+ PresetRuntimeSnapshotTracker,
4527
+ STUDIO_EVIDENCE_AGGREGATE_IDS,
4528
+ STUDIO_EVIDENCE_KINDS,
4529
+ ScopeRowIndex,
4530
+ SerialWriteQueue,
4531
+ StudioDomainError,
4532
+ StudioModelRequestCursorExpiredError,
4533
+ StudioModelRequestStore,
4534
+ StudioPresetService,
4535
+ apply,
4536
+ archiveChatLunaModelRequestError,
4537
+ buildStudioModelRequestTrajectory,
4538
+ buildStudioModelRequestTrajectoryFromStore,
4539
+ countMessageCharacters,
4540
+ countToolCallCharacters,
4541
+ countToolDefinitionCharacters,
4542
+ createModelRequestError,
4543
+ deriveModelRequestVariables,
4544
+ estimateModelRequestRecordBytes,
4545
+ expressionStableId,
4546
+ findLatestFailedModelRequest,
4547
+ getChatLunaErrorPossibleCauses,
4548
+ inferModelResponseBodyFormat,
4549
+ inject,
4550
+ installModelRequestCollector,
4551
+ isKnownChatModelRequestUrl,
4552
+ isStudioEvidenceAggregateMember,
4553
+ isVirtualOneBotSession,
4554
+ linkChatLunaUsageRequest,
4555
+ lookupChatLunaUsage,
4556
+ matchPresetExpressionEvidence,
4557
+ matchesModelRequestQuery,
4558
+ modelRequestVariableStatusLabel,
4559
+ name,
4560
+ parsePresetSourceDocument,
4561
+ presentModelRequestDetail,
4562
+ presentModelRequestListItem,
4563
+ projectModelEvidence,
4564
+ readChatLunaRequestError,
4565
+ readStudioModelRequestConversationRecords,
4566
+ readStudioSessionEntities,
4567
+ registerConsole,
4568
+ registerStudioModelRequestModel,
4569
+ resolveChatLunaPluginClass,
4570
+ resolveConsoleEntry,
4571
+ resolveModelRequestOrder,
4572
+ sanitizeModelRequestUrl,
4573
+ statusLabel,
4574
+ studioEvidenceAggregate,
4575
+ studioEvidenceLabels,
4576
+ toModelRequestRecordHeader,
4577
+ toStudioModelRequestUsage,
4578
+ usage
4579
+ });