koishi-plugin-chatluna-studio 1.1.8 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ import type { Context } from 'koishi';
2
+ /**
3
+ * 观察 chatluna-character 一轮响应真正退出的边界。
4
+ *
5
+ * Character 的 `after-chat` 只覆盖成功路径;它自己的 collect 回调则无论成功、提前返回还是抛错,都会在
6
+ * 最外层 finally 调用公开的 `releaseResponseLock(session)`。这里在每次 before-chat 时懒解析当前服务并
7
+ * 透明包装该方法,因此不需要声明注入,也能跟随 Character 后装或重载。
8
+ */
9
+ export declare function observeCharacterTurnCompletion(ctx: Context, notify: (session: unknown) => void): () => void;
@@ -72,6 +72,8 @@ export declare class ChatLunaSessionTracker {
72
72
  * 都没有,只有这张映射能把它还原成具体的(机器人,会话)。
73
73
  */
74
74
  private readonly coreKeys;
75
+ /** Character 的 session 对应到精确开始代次;失败终态只能收掉自己,不能按目标误删后继轮。 */
76
+ private readonly characterTurns;
75
77
  private readonly disposers;
76
78
  constructor(ctx: Context, options?: ChatLunaSessionTrackerOptions);
77
79
  /** 内部会话标识唯一对应一轮时才认;映射到多轮时这次错误归属不明。 */
@@ -91,7 +93,14 @@ export declare class ChatLunaSessionTracker {
91
93
  resolveActiveTurn(): ChatLunaTurnResolution;
92
94
  /** 当前正在进行的对话轮数;诊断与测试用。 */
93
95
  get activeTurnCount(): number;
96
+ /**
97
+ * 从 Character 的真实 finally 边界精确收掉对应开始代次。
98
+ *
99
+ * 同一目标的新一轮可能已覆盖活动表;此时过期 release 只消费自己的 token,不删除后继轮。
100
+ */
101
+ finishCharacterTurn(session: unknown): boolean;
94
102
  dispose(): void;
103
+ private beginCharacter;
95
104
  private begin;
96
105
  private finish;
97
106
  private finishCore;
package/lib/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Context, Schema } from 'koishi';
2
2
  import type { StudioAppearance, StudioPersistenceMode } from './types';
3
3
  export * from './console';
4
4
  export * from './console-contract';
5
+ export * from './chatluna/character-turn-completion';
5
6
  export * from './chatluna/error';
6
7
  export * from './chatluna/session-tracker';
7
8
  export * from './chatluna/usage';
package/lib/index.js CHANGED
@@ -77,6 +77,7 @@ __export(index_exports, {
77
77
  matchesModelRequestQuery: () => matchesModelRequestQuery,
78
78
  modelRequestVariableStatusLabel: () => modelRequestVariableStatusLabel,
79
79
  name: () => name,
80
+ observeCharacterTurnCompletion: () => observeCharacterTurnCompletion,
80
81
  parsePresetSourceDocument: () => parsePresetSourceDocument,
81
82
  presentModelRequestDetail: () => presentModelRequestDetail,
82
83
  presentModelRequestListItem: () => presentModelRequestListItem,
@@ -100,6 +101,137 @@ __export(index_exports, {
100
101
  module.exports = __toCommonJS(index_exports);
101
102
  var import_koishi3 = require("koishi");
102
103
 
104
+ // src/chatluna/runtime.ts
105
+ function readRecord(value) {
106
+ return value !== null && typeof value === "object" ? value : void 0;
107
+ }
108
+ function readServiceFrom(host, serviceName, accept) {
109
+ const get = readRecord(host)?.get;
110
+ if (typeof get !== "function") return void 0;
111
+ const service = readRecord(get.call(host, serviceName));
112
+ return service ? accept(service) : void 0;
113
+ }
114
+ function listRegistryContexts(value) {
115
+ const registry = readRecord(readRecord(value)?.registry);
116
+ const values = registry?.values;
117
+ if (typeof values !== "function") return [];
118
+ return [...values.call(registry)].flatMap((runtime) => {
119
+ const ctx = readRecord(runtime)?.ctx;
120
+ return ctx ? [ctx] : [];
121
+ });
122
+ }
123
+ function findChatLunaRuntime(host, serviceName, accept) {
124
+ const direct = readServiceFrom(host, serviceName, accept);
125
+ if (direct) return direct;
126
+ for (const ctx of listRegistryContexts(host)) {
127
+ const resolved = readServiceFrom(ctx, serviceName, accept);
128
+ if (resolved) return resolved;
129
+ }
130
+ }
131
+
132
+ // src/chatluna/character-turn-completion.ts
133
+ var runtimeStates = /* @__PURE__ */ new WeakMap();
134
+ function observeCharacterTurnCompletion(ctx, notify) {
135
+ const observer = {
136
+ notify,
137
+ bindings: /* @__PURE__ */ new Set(),
138
+ disposed: false
139
+ };
140
+ const on = ctx.on.bind(ctx);
141
+ const disposeListener = on("chatluna_character/before-chat", (payload) => {
142
+ if (observer.disposed) return;
143
+ const session = readRecord(payload)?.session;
144
+ if (!session || typeof session !== "object") return;
145
+ const service = findChatLunaRuntime(ctx, "chatluna_character", acceptCharacterRuntime);
146
+ if (!service) return;
147
+ const binding = selectBinding(observer, ensureRuntimeState(service));
148
+ trackSession(binding, session);
149
+ });
150
+ return () => {
151
+ if (observer.disposed) return;
152
+ observer.disposed = true;
153
+ disposeListener();
154
+ for (const binding of [...observer.bindings]) detachBinding(binding);
155
+ observer.current = void 0;
156
+ };
157
+ }
158
+ function acceptCharacterRuntime(service) {
159
+ return typeof service.releaseResponseLock === "function" ? service : void 0;
160
+ }
161
+ function ensureRuntimeState(service) {
162
+ const existing = runtimeStates.get(service);
163
+ if (existing && service.releaseResponseLock === existing.wrapper) return existing;
164
+ const original = service.releaseResponseLock;
165
+ const state = {
166
+ service,
167
+ original,
168
+ bindings: /* @__PURE__ */ new Set()
169
+ };
170
+ state.wrapper = function(session, ...args) {
171
+ for (const binding of [...state.bindings]) {
172
+ if (!consumeSession(binding, session)) continue;
173
+ try {
174
+ binding.observer.notify(session);
175
+ } catch {
176
+ }
177
+ pruneInactiveBinding(binding);
178
+ }
179
+ return original.apply(this, [session, ...args]);
180
+ };
181
+ service.releaseResponseLock = state.wrapper;
182
+ runtimeStates.set(service, state);
183
+ return state;
184
+ }
185
+ function selectBinding(observer, state) {
186
+ if (observer.current?.state === state) return observer.current;
187
+ if (observer.current) {
188
+ observer.current.current = false;
189
+ pruneInactiveBinding(observer.current);
190
+ }
191
+ let binding = [...observer.bindings].find((candidate) => candidate.state === state);
192
+ if (!binding) {
193
+ binding = {
194
+ observer,
195
+ state,
196
+ pending: /* @__PURE__ */ new WeakMap(),
197
+ pendingCount: 0,
198
+ current: true
199
+ };
200
+ observer.bindings.add(binding);
201
+ state.bindings.add(binding);
202
+ } else {
203
+ binding.current = true;
204
+ }
205
+ observer.current = binding;
206
+ return binding;
207
+ }
208
+ function trackSession(binding, session) {
209
+ binding.pending.set(session, (binding.pending.get(session) ?? 0) + 1);
210
+ binding.pendingCount += 1;
211
+ }
212
+ function consumeSession(binding, session) {
213
+ if (!session || typeof session !== "object") return false;
214
+ const count = binding.pending.get(session);
215
+ if (!count) return false;
216
+ if (count === 1) binding.pending.delete(session);
217
+ else binding.pending.set(session, count - 1);
218
+ binding.pendingCount -= 1;
219
+ return true;
220
+ }
221
+ function pruneInactiveBinding(binding) {
222
+ if (binding.current || binding.pendingCount > 0) return;
223
+ detachBinding(binding);
224
+ }
225
+ function detachBinding(binding) {
226
+ binding.state.bindings.delete(binding);
227
+ binding.observer.bindings.delete(binding);
228
+ if (binding.observer.current === binding) binding.observer.current = void 0;
229
+ if (binding.state.bindings.size > 0) return;
230
+ const { service, wrapper, original } = binding.state;
231
+ if (service.releaseResponseLock === wrapper) service.releaseResponseLock = original;
232
+ if (runtimeStates.get(service) === binding.state) runtimeStates.delete(service);
233
+ }
234
+
103
235
  // src/chatluna/error.ts
104
236
  var CHATLUNA_ERROR_CODE_DOCUMENTATION_URL = "https://chatluna.chat/guide/faq/error_code.html";
105
237
  var CHATLUNA_ERROR_CAUSES = {
@@ -116,7 +248,7 @@ var CHATLUNA_ERROR_CAUSES = {
116
248
  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
249
  };
118
250
  function readChatLunaRequestError(error) {
119
- const value = readRecord(error);
251
+ const value = readRecord2(error);
120
252
  if (!value) return;
121
253
  const code = readFiniteNumber(value.errorCode);
122
254
  const message = readNonEmptyString(value.message);
@@ -152,7 +284,7 @@ function archiveChatLunaModelRequestError(store, error, target) {
152
284
  }).catch(() => void 0);
153
285
  store.trackUpdate(task);
154
286
  }
155
- function readRecord(value) {
287
+ function readRecord2(value) {
156
288
  return value !== null && typeof value === "object" ? value : void 0;
157
289
  }
158
290
  function readNonEmptyString(value) {
@@ -162,13 +294,13 @@ function readFiniteNumber(value) {
162
294
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
163
295
  }
164
296
  function readOriginMessage(origin) {
165
- const record = readRecord(origin);
297
+ const record = readRecord2(origin);
166
298
  return readNonEmptyString(record?.message) ?? readNonEmptyString(origin);
167
299
  }
168
300
 
169
301
  // src/chatluna/session-tracker.ts
170
302
  var import_koishi = require("koishi");
171
- function readRecord2(value) {
303
+ function readRecord3(value) {
172
304
  return value !== null && typeof value === "object" ? value : void 0;
173
305
  }
174
306
  function readString(value) {
@@ -176,7 +308,7 @@ function readString(value) {
176
308
  }
177
309
  function readNested(value, path) {
178
310
  let current = value;
179
- for (const key of path) current = readRecord2(current)?.[key];
311
+ for (const key of path) current = readRecord3(current)?.[key];
180
312
  return current;
181
313
  }
182
314
  function readFirstString(value, paths) {
@@ -233,10 +365,10 @@ var ChatLunaSessionTracker = class {
233
365
  this.options = options;
234
366
  const on = ctx.on.bind(ctx);
235
367
  this.disposers.push(on("chatluna/before-chat", (conversationId, _message, _variables, _chatInterface, session) => {
236
- const key = this.begin(session);
237
- if (!key) return;
368
+ const started = this.begin(session);
369
+ if (!started) return;
238
370
  const keys = this.coreKeys.get(conversationId) ?? /* @__PURE__ */ new Set();
239
- keys.add(key);
371
+ keys.add(started.key);
240
372
  this.coreKeys.set(conversationId, keys);
241
373
  }));
242
374
  this.disposers.push(on("chatluna/after-chat", (conversationId, _source, _response, _variables, _chatInterface, session) => {
@@ -248,10 +380,11 @@ var ChatLunaSessionTracker = class {
248
380
  this.finishCore(conversationId);
249
381
  }));
250
382
  this.disposers.push(on("chatluna_character/before-chat", (payload) => {
251
- this.begin(readRecord2(payload)?.session);
383
+ this.beginCharacter(readRecord3(payload)?.session);
252
384
  }));
253
385
  this.disposers.push(on("chatluna_character/after-chat", (payload) => {
254
- this.finish(readRecord2(payload)?.session);
386
+ const session = readRecord3(payload)?.session;
387
+ if (!this.finishCharacterTurn(session)) this.finish(session);
255
388
  }));
256
389
  }
257
390
  options;
@@ -264,6 +397,8 @@ var ChatLunaSessionTracker = class {
264
397
  * 都没有,只有这张映射能把它还原成具体的(机器人,会话)。
265
398
  */
266
399
  coreKeys = /* @__PURE__ */ new Map();
400
+ /** Character 的 session 对应到精确开始代次;失败终态只能收掉自己,不能按目标误删后继轮。 */
401
+ characterTurns = /* @__PURE__ */ new WeakMap();
267
402
  disposers = [];
268
403
  /** 内部会话标识唯一对应一轮时才认;映射到多轮时这次错误归属不明。 */
269
404
  resolveCoreTurn(conversationId) {
@@ -296,22 +431,44 @@ var ChatLunaSessionTracker = class {
296
431
  get activeTurnCount() {
297
432
  return this.turns.size;
298
433
  }
434
+ /**
435
+ * 从 Character 的真实 finally 边界精确收掉对应开始代次。
436
+ *
437
+ * 同一目标的新一轮可能已覆盖活动表;此时过期 release 只消费自己的 token,不删除后继轮。
438
+ */
439
+ finishCharacterTurn(session) {
440
+ if (!session || typeof session !== "object") return false;
441
+ const tokens = this.characterTurns.get(session);
442
+ const token = tokens?.shift();
443
+ if (!token) return false;
444
+ if (!tokens?.length) this.characterTurns.delete(session);
445
+ if (this.turns.get(token.key) === token.turn) this.turns.delete(token.key);
446
+ return true;
447
+ }
299
448
  dispose() {
300
449
  for (const dispose of this.disposers.splice(0)) dispose();
301
450
  this.turns.clear();
302
451
  this.coreKeys.clear();
303
452
  }
453
+ beginCharacter(session) {
454
+ const started = this.begin(session);
455
+ if (!started || !session || typeof session !== "object") return;
456
+ const tokens = this.characterTurns.get(session) ?? [];
457
+ tokens.push(started);
458
+ this.characterTurns.set(session, tokens);
459
+ }
304
460
  begin(session) {
305
461
  const entities = readStudioSessionEntities(session);
306
462
  if (!entities) return;
307
463
  const key = turnKey(entities);
308
464
  const existing = this.turns.get(key);
309
- this.turns.set(key, {
465
+ const turn = {
310
466
  entities,
311
467
  interactionId: existing?.interactionId ?? import_koishi.Random.id(),
312
468
  virtual: isVirtualOneBotSession(session)
313
- });
314
- return key;
469
+ };
470
+ this.turns.set(key, turn);
471
+ return { key, turn };
315
472
  }
316
473
  finish(session) {
317
474
  const entities = readStudioSessionEntities(session);
@@ -4166,34 +4323,6 @@ function normalizeFileSystemError(error, fileName) {
4166
4323
  return new PresetRepositoryError("unsafe-file", `\u65E0\u6CD5\u5B89\u5168\u8BBF\u95EE\u9884\u8BBE\uFF1A${fileName}`, error);
4167
4324
  }
4168
4325
 
4169
- // src/chatluna/runtime.ts
4170
- function readRecord3(value) {
4171
- return value !== null && typeof value === "object" ? value : void 0;
4172
- }
4173
- function readServiceFrom(host, serviceName, accept) {
4174
- const get = readRecord3(host)?.get;
4175
- if (typeof get !== "function") return void 0;
4176
- const service = readRecord3(get.call(host, serviceName));
4177
- return service ? accept(service) : void 0;
4178
- }
4179
- function listRegistryContexts(value) {
4180
- const registry = readRecord3(readRecord3(value)?.registry);
4181
- const values = registry?.values;
4182
- if (typeof values !== "function") return [];
4183
- return [...values.call(registry)].flatMap((runtime) => {
4184
- const ctx = readRecord3(runtime)?.ctx;
4185
- return ctx ? [ctx] : [];
4186
- });
4187
- }
4188
- function findChatLunaRuntime(host, serviceName, accept) {
4189
- const direct = readServiceFrom(host, serviceName, accept);
4190
- if (direct) return direct;
4191
- for (const ctx of listRegistryContexts(host)) {
4192
- const resolved = readServiceFrom(ctx, serviceName, accept);
4193
- if (resolved) return resolved;
4194
- }
4195
- }
4196
-
4197
4326
  // src/presets/runtime-correlation.ts
4198
4327
  var PresetRuntimeSnapshotTracker = class {
4199
4328
  constructor(ctx) {
@@ -4231,23 +4360,17 @@ var PresetRuntimeSnapshotTracker = class {
4231
4360
  this.active.set(key, { target, turnId, snapshot });
4232
4361
  const session = record?.session;
4233
4362
  if (session && typeof session === "object") {
4234
- const keys = this.characterSessionTurnKeys.get(session) ?? /* @__PURE__ */ new Set();
4235
- keys.add(key);
4363
+ const keys = this.characterSessionTurnKeys.get(session) ?? [];
4364
+ keys.push(key);
4236
4365
  this.characterSessionTurnKeys.set(session, keys);
4237
4366
  }
4238
4367
  }));
4239
4368
  this.disposers.push(on("chatluna_character/after-chat", (payload) => {
4240
4369
  const record = readRecord4(payload);
4241
- const target = this.resolveSession(record?.session);
4242
- if (!target) return;
4243
4370
  const session = record?.session;
4244
- const sessionKeys = session && typeof session === "object" ? this.characterSessionTurnKeys.get(session) : void 0;
4245
- if (session && typeof session === "object" && sessionKeys?.size === 1) {
4246
- const [directKey] = sessionKeys;
4247
- if (directKey) this.active.delete(directKey);
4248
- this.characterSessionTurnKeys.delete(session);
4249
- return;
4250
- }
4371
+ if (this.finishCharacterTurn(session)) return;
4372
+ const target = this.resolveSession(session);
4373
+ if (!target) return;
4251
4374
  const matches = [...this.active].filter(([, turn]) => turn.snapshot.kind === "character" && sameTarget(turn.target, target));
4252
4375
  if (matches.length === 1) this.active.delete(matches[0][0]);
4253
4376
  }));
@@ -4255,10 +4378,21 @@ var PresetRuntimeSnapshotTracker = class {
4255
4378
  ctx;
4256
4379
  active = /* @__PURE__ */ new Map();
4257
4380
  coreTurnKeys = /* @__PURE__ */ new Map();
4381
+ /** Character 同一 session 上的开始代次按顺序保存,终态只能删除与自己对应的快照。 */
4258
4382
  characterSessionTurnKeys = /* @__PURE__ */ new WeakMap();
4259
4383
  disposers = [];
4260
4384
  nextCoreTurn = 1;
4261
4385
  nextCharacterTurn = 1;
4386
+ /** Character 的 release 终态只删除该 session 最早尚未完成的快照代次。 */
4387
+ finishCharacterTurn(session) {
4388
+ if (!session || typeof session !== "object") return false;
4389
+ const keys = this.characterSessionTurnKeys.get(session);
4390
+ const key = keys?.shift();
4391
+ if (!key) return false;
4392
+ if (!keys?.length) this.characterSessionTurnKeys.delete(session);
4393
+ this.active.delete(key);
4394
+ return true;
4395
+ }
4262
4396
  getActiveSnapshots(target) {
4263
4397
  const turns = [...this.active.values()].filter((turn) => sameTarget(turn.target, target));
4264
4398
  const snapshots = ["core", "character"].flatMap((kind) => {
@@ -4609,6 +4743,10 @@ function apply(ctx, config) {
4609
4743
  if (conversationId) archiveChatLunaModelRequestError(modelRequests, error, { conversationId });
4610
4744
  }
4611
4745
  });
4746
+ const disposeCharacterTurnCompletion = observeCharacterTurnCompletion(inner, (session) => {
4747
+ sessions.finishCharacterTurn(session);
4748
+ presetSnapshots.finishCharacterTurn(session);
4749
+ });
4612
4750
  const presets = new StudioPresetService({ baseDir: inner.baseDir, modelRequests });
4613
4751
  const getPersistenceStatus = () => {
4614
4752
  if (config.persistenceMode !== "database") {
@@ -4653,6 +4791,7 @@ function apply(ctx, config) {
4653
4791
  });
4654
4792
  inner.on("dispose", () => {
4655
4793
  disposeCollector();
4794
+ disposeCharacterTurnCompletion();
4656
4795
  presetSnapshots.dispose();
4657
4796
  sessions.dispose();
4658
4797
  void modelRequests.waitForPersistence().catch((error) => {
@@ -4710,6 +4849,7 @@ function apply(ctx, config) {
4710
4849
  matchesModelRequestQuery,
4711
4850
  modelRequestVariableStatusLabel,
4712
4851
  name,
4852
+ observeCharacterTurnCompletion,
4713
4853
  parsePresetSourceDocument,
4714
4854
  presentModelRequestDetail,
4715
4855
  presentModelRequestListItem,
@@ -4,11 +4,14 @@ export declare class PresetRuntimeSnapshotTracker {
4
4
  private ctx;
5
5
  private readonly active;
6
6
  private readonly coreTurnKeys;
7
+ /** Character 同一 session 上的开始代次按顺序保存,终态只能删除与自己对应的快照。 */
7
8
  private readonly characterSessionTurnKeys;
8
9
  private readonly disposers;
9
10
  private nextCoreTurn;
10
11
  private nextCharacterTurn;
11
12
  constructor(ctx: Context);
13
+ /** Character 的 release 终态只删除该 session 最早尚未完成的快照代次。 */
14
+ finishCharacterTurn(session: unknown): boolean;
12
15
  getActiveSnapshots(target: PresetRuntimeSessionTarget): PresetRuntimeSnapshot[];
13
16
  dispose(): void;
14
17
  private resolveSession;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chatluna-studio",
3
3
  "description": "在 Koishi 控制台中查看 ChatLuna 真实模型请求并编辑预设",
4
- "version": "1.1.8",
4
+ "version": "1.1.9",
5
5
  "type": "commonjs",
6
6
  "main": "lib/index.js",
7
7
  "typings": "lib/index.d.ts",