@axiom-lattice/gateway 6.0.1 → 6.1.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 (26) hide show
  1. package/dist/embed/{embed-page-AgentDetailSideApp-JYVZKSUU.js → embed-page-AgentDetailSideApp-2UFH4RJG.js} +1 -1
  2. package/dist/embed/{embed-page-AgentGovernanceWorkbench-SD2EHW2B.js → embed-page-AgentGovernanceWorkbench-3JOSRW2V.js} +1 -1
  3. package/dist/embed/{embed-page-MessageGraph-GLQXKAVC.js → embed-page-MessageGraph-SPB75EHC.js} +1 -1
  4. package/dist/embed/{embed-page-ReactChart-74MHCE2K.js → embed-page-ReactChart-EE4A5WKS.js} +1 -1
  5. package/dist/embed/{embed-page-SkillFlow-NDAK3GAD.js → embed-page-SkillFlow-H2ARZGEX.js} +1 -1
  6. package/dist/embed/{embed-page-TasksSideApp-DA4LUTMD.js → embed-page-TasksSideApp-BQMOLOUF.js} +1 -1
  7. package/dist/embed/{embed-page-TeamWorkspace-DUB3L6V6.js → embed-page-TeamWorkspace-QXMRVXWU.js} +1 -1
  8. package/dist/embed/{embed-page-TopologyRuntimeView-NFBHAICN.js → embed-page-TopologyRuntimeView-OSV75P6Y.js} +1 -1
  9. package/dist/embed/{embed-page-WorkflowAutomationView-43YGTXGF.js → embed-page-WorkflowAutomationView-KJ6HJ5UT.js} +1 -1
  10. package/dist/embed/{embed-page-chunk-UHH575CK.js → embed-page-chunk-DBUC6J4G.js} +1 -1
  11. package/dist/embed/{embed-page-chunk-C4ZV4IXK.js → embed-page-chunk-E6F5YZBR.js} +1 -1
  12. package/dist/embed/{embed-page-chunk-BVSC3MSO.js → embed-page-chunk-FGKHXQAJ.js} +1 -1
  13. package/dist/embed/{embed-page-chunk-2PFKLMTR.js → embed-page-chunk-GSCVATOA.js} +1 -1
  14. package/dist/embed/{embed-page-chunk-GKHIPXZ2.js → embed-page-chunk-IIEDLN7W.js} +1 -1
  15. package/dist/embed/{embed-page-chunk-P477ZTUO.js → embed-page-chunk-U4XSGC4Y.js} +163 -163
  16. package/dist/embed/{embed-page-chunk-DWIBLGBW.js → embed-page-chunk-VXWRPT33.js} +1 -1
  17. package/dist/embed/{embed-page-en-S4MXFPIU.js → embed-page-en-X6B6RVNL.js} +1 -1
  18. package/dist/embed/{embed-page-zh-SSMSF5LE.js → embed-page-zh-EQ5X2M32.js} +1 -1
  19. package/dist/embed/embed-page.js +1 -1
  20. package/dist/index.d.mts +43 -8
  21. package/dist/index.d.ts +43 -8
  22. package/dist/index.js +1117 -365
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.mjs +907 -148
  25. package/dist/index.mjs.map +1 -1
  26. package/package.json +7 -7
package/dist/index.mjs CHANGED
@@ -2678,13 +2678,13 @@ async function searchEntries(request, reply) {
2678
2678
 
2679
2679
  // src/controllers/tools.ts
2680
2680
  import { toolLatticeManager } from "@axiom-lattice/core";
2681
- function serializeSchema(schema) {
2682
- if (!schema) {
2681
+ function serializeSchema(schema2) {
2682
+ if (!schema2) {
2683
2683
  return void 0;
2684
2684
  }
2685
2685
  try {
2686
- if (schema._def) {
2687
- const def = schema._def;
2686
+ if (schema2._def) {
2687
+ const def = schema2._def;
2688
2688
  if (def.typeName === "ZodObject") {
2689
2689
  const shape = def.shape();
2690
2690
  const properties = {};
@@ -2710,7 +2710,7 @@ function serializeSchema(schema) {
2710
2710
  }
2711
2711
  return {
2712
2712
  type: "object",
2713
- description: schema.description || "Schema definition"
2713
+ description: schema2.description || "Schema definition"
2714
2714
  };
2715
2715
  } catch (error) {
2716
2716
  return {
@@ -11401,7 +11401,50 @@ function registerA2ABridgeRoutes(app2) {
11401
11401
  }
11402
11402
 
11403
11403
  // src/routes/stt.ts
11404
- import { getSTTClient, sttModelLatticeManager } from "@axiom-lattice/core";
11404
+ import { sttModelLatticeManager } from "@axiom-lattice/core";
11405
+
11406
+ // src/controllers/tenant-scope.ts
11407
+ var TenantScopeError2 = class extends Error {
11408
+ /** Creates a tenant scope error. */
11409
+ constructor(code, statusCode, message) {
11410
+ super(message);
11411
+ this.code = code;
11412
+ this.statusCode = statusCode;
11413
+ this.name = "TenantScopeError";
11414
+ }
11415
+ };
11416
+ function resolveTenantScope(request, authRequired2) {
11417
+ const authenticated = request.user?.tenantId;
11418
+ const header2 = request.headers["x-tenant-id"];
11419
+ const requested = Array.isArray(header2) ? header2[0] : header2;
11420
+ if (authenticated) {
11421
+ if (requested && requested !== authenticated) {
11422
+ throw new TenantScopeError2(
11423
+ "TENANT_ACCESS_DENIED",
11424
+ 403,
11425
+ "Tenant access denied"
11426
+ );
11427
+ }
11428
+ return { tenantId: authenticated, userId: request.user?.id };
11429
+ }
11430
+ if (authRequired2) {
11431
+ throw new TenantScopeError2(
11432
+ "UNAUTHENTICATED",
11433
+ 401,
11434
+ "Console session required"
11435
+ );
11436
+ }
11437
+ return { tenantId: requested || "default" };
11438
+ }
11439
+
11440
+ // src/routes/stt.ts
11441
+ function tenantIdOf(request) {
11442
+ try {
11443
+ return resolveTenantScope(request, false).tenantId;
11444
+ } catch {
11445
+ return "default";
11446
+ }
11447
+ }
11405
11448
  var ALLOWED_AUDIO_FORMATS = /* @__PURE__ */ new Set([
11406
11449
  "webm",
11407
11450
  "wav",
@@ -11455,10 +11498,11 @@ var registerSTTRoutes = (app2) => {
11455
11498
  return reply.status(400).send({ error: `Unsupported audio format: ${format}` });
11456
11499
  }
11457
11500
  try {
11458
- let client;
11459
- try {
11460
- client = getSTTClient(modelKey2);
11461
- } catch {
11501
+ const client = await sttModelLatticeManager.getSTTClientWithTenantAsync(
11502
+ tenantIdOf(request),
11503
+ modelKey2
11504
+ );
11505
+ if (!client) {
11462
11506
  return reply.status(404).send({ error: `STT model "${modelKey2}" not found` });
11463
11507
  }
11464
11508
  const result = await client.transcribe(buffer, format);
@@ -11752,6 +11796,9 @@ function registerTaskRoutes(app2, controller) {
11752
11796
  import {
11753
11797
  agentInstanceManager as agentInstanceManager6,
11754
11798
  agentLatticeManager as agentLatticeManager4,
11799
+ embeddingsLatticeManager as embeddingsLatticeManager2,
11800
+ sttModelLatticeManager as sttModelLatticeManager2,
11801
+ vectorStoreLatticeManager,
11755
11802
  createProjectTaskReassignmentAuthorizer,
11756
11803
  createTaskLifecycleService,
11757
11804
  createTrustedAgentMessageDispatcher,
@@ -15721,6 +15768,7 @@ import { toModelKey } from "@axiom-lattice/protocols";
15721
15768
  import {
15722
15769
  assertSafeUpstreamUrl,
15723
15770
  buildModelLlmConfig,
15771
+ DEFAULT_MODEL_EXTRA,
15724
15772
  UnsupportedModelProviderError,
15725
15773
  UnsafeUpstreamUrlError
15726
15774
  } from "@axiom-lattice/core";
@@ -15763,13 +15811,48 @@ function resolveDiscoveryTimeoutMs(raw = process.env.LATTICE_MODEL_DISCOVERY_TIM
15763
15811
  return parsed;
15764
15812
  }
15765
15813
  function buildModelsEndpoint(baseURL) {
15814
+ return buildOpenAiEndpoint(baseURL, "models");
15815
+ }
15816
+ function buildOpenAiEndpoint(baseURL, path5) {
15766
15817
  const url = new URL(baseURL);
15767
15818
  const trimmed = url.pathname.replace(/\/+$/, "");
15768
- url.pathname = trimmed.endsWith("/v1") ? `${trimmed}/models` : `${trimmed}/v1/models`;
15819
+ url.pathname = /\/v\d+$/.test(trimmed) ? `${trimmed}/${path5}` : `${trimmed}/v1/${path5}`;
15769
15820
  url.search = "";
15770
15821
  url.hash = "";
15771
15822
  return url.toString();
15772
15823
  }
15824
+ function assertUpstreamOk(response) {
15825
+ if (response.status >= 300 && response.status < 400) {
15826
+ throw new ModelProviderServiceError(
15827
+ "UPSTREAM_REDIRECT_BLOCKED",
15828
+ 502,
15829
+ "Upstream returned a redirect; configure the final URL directly"
15830
+ );
15831
+ }
15832
+ if (response.status === 401 || response.status === 403) {
15833
+ throw new ModelProviderServiceError(
15834
+ "UPSTREAM_AUTH_FAILED",
15835
+ 502,
15836
+ "Upstream rejected the configured credentials"
15837
+ );
15838
+ }
15839
+ if (response.status === 429) {
15840
+ throw new ModelProviderServiceError(
15841
+ "UPSTREAM_RATE_LIMITED",
15842
+ 502,
15843
+ "Upstream rate limited the request",
15844
+ true
15845
+ );
15846
+ }
15847
+ if (!response.ok) {
15848
+ throw new ModelProviderServiceError(
15849
+ "UPSTREAM_UNREACHABLE",
15850
+ 502,
15851
+ "Upstream returned an error response",
15852
+ response.status >= 500
15853
+ );
15854
+ }
15855
+ }
15773
15856
  function parseModelsPayload(payload) {
15774
15857
  const entries = Array.isArray(payload) ? payload : typeof payload === "object" && payload !== null ? payload.data : void 0;
15775
15858
  if (!Array.isArray(entries)) {
@@ -15840,36 +15923,60 @@ var openAiCompatibleAdapter = {
15840
15923
  true
15841
15924
  );
15842
15925
  }
15843
- if (response.status >= 300 && response.status < 400) {
15844
- throw new ModelProviderServiceError(
15845
- "UPSTREAM_REDIRECT_BLOCKED",
15846
- 502,
15847
- "Upstream returned a redirect; configure the final URL directly"
15848
- );
15849
- }
15850
- if (response.status === 401 || response.status === 403) {
15851
- throw new ModelProviderServiceError(
15852
- "UPSTREAM_AUTH_FAILED",
15853
- 502,
15854
- "Upstream rejected the configured credentials"
15855
- );
15856
- }
15857
- if (response.status === 429) {
15926
+ assertUpstreamOk(response);
15927
+ const body = await readBodyWithLimit(response, MAX_RESPONSE_BYTES);
15928
+ let payload;
15929
+ try {
15930
+ payload = JSON.parse(body);
15931
+ } catch {
15858
15932
  throw new ModelProviderServiceError(
15859
- "UPSTREAM_RATE_LIMITED",
15933
+ "UPSTREAM_INVALID_RESPONSE",
15860
15934
  502,
15861
- "Upstream rate limited the request",
15862
- true
15935
+ "Upstream response was not valid JSON"
15863
15936
  );
15864
15937
  }
15865
- if (!response.ok) {
15938
+ return parseModelsPayload(payload);
15939
+ }
15940
+ };
15941
+ async function probeEmbeddingInference(input, signal) {
15942
+ const startedAt = Date.now();
15943
+ const endpoint = buildOpenAiEndpoint(input.baseURL, "embeddings");
15944
+ const headers = {
15945
+ Accept: "application/json",
15946
+ "Content-Type": "application/json"
15947
+ };
15948
+ if (input.apiKey) {
15949
+ headers.Authorization = `Bearer ${input.apiKey}`;
15950
+ }
15951
+ try {
15952
+ let response;
15953
+ try {
15954
+ response = await fetch(endpoint, {
15955
+ method: "POST",
15956
+ headers,
15957
+ // Never follow redirects: a redirected request could reach an address
15958
+ // that bypassed the SSRF checks.
15959
+ redirect: "manual",
15960
+ signal,
15961
+ body: JSON.stringify({ model: input.modelId, input: "ping" })
15962
+ });
15963
+ } catch (error) {
15964
+ if (error instanceof Error && error.name === "AbortError") {
15965
+ throw new ModelProviderServiceError(
15966
+ "UPSTREAM_TIMEOUT",
15967
+ 504,
15968
+ "Upstream did not respond in time",
15969
+ true
15970
+ );
15971
+ }
15866
15972
  throw new ModelProviderServiceError(
15867
15973
  "UPSTREAM_UNREACHABLE",
15868
15974
  502,
15869
- "Upstream returned an error response",
15870
- response.status >= 500
15975
+ "Upstream could not be reached",
15976
+ true
15871
15977
  );
15872
15978
  }
15979
+ assertUpstreamOk(response);
15873
15980
  const body = await readBodyWithLimit(response, MAX_RESPONSE_BYTES);
15874
15981
  let payload;
15875
15982
  try {
@@ -15881,9 +15988,37 @@ var openAiCompatibleAdapter = {
15881
15988
  "Upstream response was not valid JSON"
15882
15989
  );
15883
15990
  }
15884
- return parseModelsPayload(payload);
15991
+ return {
15992
+ reachable: true,
15993
+ latencyMs: Date.now() - startedAt,
15994
+ dimensions: readEmbeddingDimensions(payload)
15995
+ };
15996
+ } catch (error) {
15997
+ if (error instanceof ModelProviderServiceError && isUpstreamErrorCode(error.code)) {
15998
+ return {
15999
+ reachable: false,
16000
+ latencyMs: Date.now() - startedAt,
16001
+ error: error.code
16002
+ };
16003
+ }
16004
+ throw error;
15885
16005
  }
15886
- };
16006
+ }
16007
+ function readEmbeddingDimensions(payload) {
16008
+ if (typeof payload !== "object" || payload === null) {
16009
+ return void 0;
16010
+ }
16011
+ const data = payload.data;
16012
+ if (!Array.isArray(data) || data.length === 0) {
16013
+ return void 0;
16014
+ }
16015
+ const first = data[0];
16016
+ if (typeof first !== "object" || first === null) {
16017
+ return void 0;
16018
+ }
16019
+ const embedding = first.embedding;
16020
+ return Array.isArray(embedding) ? embedding.length : void 0;
16021
+ }
15887
16022
  async function readBodyWithLimit(response, maxBytes) {
15888
16023
  if (!response.body) {
15889
16024
  return response.text();
@@ -15985,6 +16120,9 @@ var ModelProviderService = class {
15985
16120
  }
15986
16121
  const models = snapshot.map((model) => ({
15987
16122
  modelKey: toModelKey(model.providerId, model.modelId),
16123
+ // What this row can do. Pickers filter on it, so it travels with every
16124
+ // entry rather than being re-derived by each consumer.
16125
+ kind: model.kind,
15988
16126
  displayName: model.displayName,
15989
16127
  providerId: model.providerId,
15990
16128
  providerName: providerNames.get(model.providerId) ?? model.providerId,
@@ -16012,6 +16150,9 @@ var ModelProviderService = class {
16012
16150
  // which is how the deployment's `default` alias came to be offered as
16013
16151
  // a model literally called "default".
16014
16152
  displayName: config.displayName || config.model || modelKey2,
16153
+ // Deployment-registered models are chat by construction: they are
16154
+ // built with `registerModelLattice`, which only mints LLM clients.
16155
+ kind: "chat",
16015
16156
  providerId: "",
16016
16157
  providerName: "registered",
16017
16158
  source: "registered",
@@ -16065,6 +16206,8 @@ var ModelProviderService = class {
16065
16206
  * left untouched.
16066
16207
  */
16067
16208
  async reconcileProvider(tenantId, providerId2) {
16209
+ this.deps.registry.evictProviderCache?.(tenantId, providerId2);
16210
+ this.deps.registry.forgetTenantDefaultMiss?.(tenantId);
16068
16211
  if (!this.shouldRegister(tenantId)) {
16069
16212
  return;
16070
16213
  }
@@ -16074,21 +16217,9 @@ var ModelProviderService = class {
16074
16217
  }
16075
16218
  const provider = await this.deps.providerStore.getProviderById(tenantId, providerId2);
16076
16219
  if (!provider) {
16077
- const prefix = `${providerId2}/`;
16078
- for (const { modelKey: modelKey2 } of this.deps.registry.listRegistered(tenantId)) {
16079
- if (modelKey2.startsWith(prefix)) {
16080
- this.deps.registry.remove(tenantId, modelKey2);
16081
- }
16082
- }
16083
16220
  return;
16084
16221
  }
16085
16222
  const models = await this.deps.providerStore.listModels(tenantId, providerId2);
16086
- const current = new Set(models.map((model) => toModelKey(providerId2, model.modelId)));
16087
- for (const { modelKey: modelKey2 } of this.deps.registry.listRegistered(tenantId)) {
16088
- if (modelKey2.startsWith(`${providerId2}/`) && !current.has(modelKey2)) {
16089
- this.deps.registry.remove(tenantId, modelKey2);
16090
- }
16091
- }
16092
16223
  this.syncRegistry(tenantId, provider, models);
16093
16224
  }
16094
16225
  /**
@@ -16187,11 +16318,9 @@ var ModelProviderService = class {
16187
16318
  async remove(userId, tenantId, id3) {
16188
16319
  await this.assertAdmin(userId, tenantId);
16189
16320
  await this.requireProvider(tenantId, id3);
16190
- const snapshot = await this.deps.providerStore.listModels(tenantId, id3);
16191
16321
  await this.deps.providerStore.deleteProvider(tenantId, id3);
16192
- for (const model of snapshot) {
16193
- this.deps.registry.remove(tenantId, toModelKey(id3, model.modelId));
16194
- }
16322
+ this.deps.registry.evictProviderCache?.(tenantId, id3);
16323
+ this.deps.registry.forgetTenantDefaultMiss?.(tenantId);
16195
16324
  this.notifyChanged(tenantId, id3);
16196
16325
  }
16197
16326
  /**
@@ -16221,7 +16350,8 @@ var ModelProviderService = class {
16221
16350
  () => controller.abort(),
16222
16351
  resolveDiscoveryTimeoutMs()
16223
16352
  );
16224
- const previous = this.shouldRegister(tenantId) ? await this.deps.providerStore.listModels(tenantId, id3) : [];
16353
+ const previous = await this.deps.providerStore.listModels(tenantId, id3);
16354
+ const previousIds = new Set(previous.map((model) => model.modelId));
16225
16355
  let discovered;
16226
16356
  try {
16227
16357
  discovered = await adapter.discover({
@@ -16247,7 +16377,12 @@ var ModelProviderService = class {
16247
16377
  const inserted = await this.deps.providerStore.replaceProviderModels(
16248
16378
  tenantId,
16249
16379
  id3,
16250
- discovered
16380
+ discovered.map((model) => ({
16381
+ ...model,
16382
+ // Same rule as `setModels`: a row this provider just brought in starts
16383
+ // with the default config, a row already stored keeps the tenant's.
16384
+ extra: previousIds.has(model.id) ? void 0 : { ...DEFAULT_MODEL_EXTRA }
16385
+ }))
16251
16386
  );
16252
16387
  await this.deps.providerStore.markProviderStatus(
16253
16388
  tenantId,
@@ -16321,6 +16456,30 @@ var ModelProviderService = class {
16321
16456
  resolveDiscoveryTimeoutMs()
16322
16457
  );
16323
16458
  const startedAt = Date.now();
16459
+ if (input.kind === "embedding") {
16460
+ if (!input.modelId) {
16461
+ throw new ModelProviderServiceError(
16462
+ "INVALID_INPUT",
16463
+ 400,
16464
+ "An embedding probe needs a model id"
16465
+ );
16466
+ }
16467
+ try {
16468
+ const outcome = await probeEmbeddingInference(
16469
+ { baseURL: input.baseURL, apiKey, modelId: input.modelId },
16470
+ controller.signal
16471
+ );
16472
+ return {
16473
+ reachable: outcome.reachable,
16474
+ latencyMs: outcome.latencyMs,
16475
+ models: [],
16476
+ ...outcome.dimensions === void 0 ? {} : { dimensions: outcome.dimensions },
16477
+ ...outcome.error ? { error: outcome.error } : {}
16478
+ };
16479
+ } finally {
16480
+ clearTimeout(timeout);
16481
+ }
16482
+ }
16324
16483
  try {
16325
16484
  const models = await adapter.discover({
16326
16485
  baseURL: input.baseURL,
@@ -16385,8 +16544,13 @@ var ModelProviderService = class {
16385
16544
  ownedBy: previousById.get(entry.modelId)?.ownedBy,
16386
16545
  // An omitted `extra` means "keep what is stored" — the picker sends no
16387
16546
  // config, and a sync must not wipe one set by hand. The store resolves
16388
- // the fallback against the row's current value.
16389
- extra: entry.extra,
16547
+ // the fallback against the row's current value. A model that is not
16548
+ // stored at all gets the default config rather than an empty one.
16549
+ extra: entry.extra ?? (previousById.has(entry.modelId) ? void 0 : { ...DEFAULT_MODEL_EXTRA }),
16550
+ // Omitted means "keep the stored kind"; the store defaults a new row
16551
+ // to chat. A picker that only toggles `enabled` therefore cannot
16552
+ // accidentally reclassify a model.
16553
+ kind: entry.kind,
16390
16554
  // Same rule: omitted leaves the flag alone, so a sync cannot quietly
16391
16555
  // move the tenant's default.
16392
16556
  isDefault: entry.isDefault
@@ -16409,14 +16573,17 @@ var ModelProviderService = class {
16409
16573
  return this.toPublicEntry(provider, models.length);
16410
16574
  }
16411
16575
  /**
16412
- * Give a tenant's sole model the default flag.
16576
+ * Give a tenant's sole **chat** model the default flag.
16413
16577
  *
16414
- * A tenant with exactly one runnable model has an obvious default, and saying
16415
- * so is what keeps the invariant the rest of the feature leans on: a tenant
16416
- * either has no models at all — and falls back to the deployment's default —
16417
- * or has a default, which cannot be deleted. Without this a tenant could sit
16418
- * with one model and no default, and deleting that model would leave nothing
16419
- * behind.
16578
+ * A tenant with exactly one runnable chat model has an obvious default, and
16579
+ * saying so is what keeps the invariant the rest of the feature leans on: a
16580
+ * tenant either has no chat model at all — and falls back to the deployment's
16581
+ * default — or has a default, which cannot be deleted. Without this a tenant
16582
+ * could sit with one model and no default, and deleting that model would
16583
+ * leave nothing behind.
16584
+ *
16585
+ * Chat only: a bare `"default"` key resolves to a chat client, so an
16586
+ * embedding or ASR row must never be marked as one.
16420
16587
  *
16421
16588
  * Deliberately narrow: more than one model is the tenant's choice to make,
16422
16589
  * and a disabled model is not a candidate for "the model this tenant runs".
@@ -16425,24 +16592,33 @@ var ModelProviderService = class {
16425
16592
  */
16426
16593
  async ensureDefaultModel(tenantId) {
16427
16594
  const models = await this.deps.providerStore.listModels(tenantId);
16428
- if (models.length !== 1) {
16595
+ const chatModels = models.filter((model) => model.kind === "chat");
16596
+ if (chatModels.length !== 1) {
16429
16597
  return;
16430
16598
  }
16431
- const [only] = models;
16599
+ const [only] = chatModels;
16432
16600
  if (only.isDefault || !only.enabled) {
16433
16601
  return;
16434
16602
  }
16435
- await this.deps.providerStore.replaceProviderModels(tenantId, only.providerId, [
16436
- {
16437
- id: only.modelId,
16438
- enabled: only.enabled,
16439
- stale: only.stale,
16440
- extra: only.extra,
16441
- isDefault: true
16442
- }
16443
- ]);
16603
+ const siblings = models.filter(
16604
+ (model) => model.providerId === only.providerId
16605
+ );
16606
+ await this.deps.providerStore.replaceProviderModels(
16607
+ tenantId,
16608
+ only.providerId,
16609
+ siblings.map((model) => ({
16610
+ id: model.modelId,
16611
+ enabled: model.enabled,
16612
+ stale: model.stale,
16613
+ extra: model.extra,
16614
+ kind: model.kind,
16615
+ isDefault: model.modelId === only.modelId
16616
+ }))
16617
+ );
16444
16618
  }
16445
16619
  syncRegistry(tenantId, provider, models) {
16620
+ this.deps.registry.evictProviderCache?.(tenantId, provider.id);
16621
+ this.deps.registry.forgetTenantDefaultMiss?.(tenantId);
16446
16622
  if (!this.shouldRegister(tenantId)) {
16447
16623
  return [];
16448
16624
  }
@@ -16689,6 +16865,10 @@ var setModelsSchema = z9.object({
16689
16865
  // Omitted means "leave the stored config alone" — the picker sends
16690
16866
  // only ids and states, and must not wipe a config set by hand.
16691
16867
  extra: extraSchema.optional(),
16868
+ // What this model does. Omitted means "leave the stored kind
16869
+ // alone"; a new row defaults to chat. The kinds are independent —
16870
+ // each has its own runtime lattice and loader guard.
16871
+ kind: z9.enum(["chat", "embedding", "asr"]).optional(),
16692
16872
  // At most one per tenant; the service rejects a tie with a
16693
16873
  // readable error rather than letting the unique index raise one.
16694
16874
  // Omitted means "leave the stored flag alone", which is what a
@@ -16701,43 +16881,13 @@ var probeProviderSchema = z9.object({
16701
16881
  baseURL: z9.string().trim().min(1).max(2048),
16702
16882
  apiKey: z9.string().max(1024).optional(),
16703
16883
  providerId: z9.string().trim().min(1).max(255).optional(),
16704
- protocol: protocolSchema.optional()
16884
+ protocol: protocolSchema.optional(),
16885
+ // Only an embedding probe uses these: it names the model and asks for one
16886
+ // inference, because a model list cannot prove a model can embed.
16887
+ kind: z9.enum(["chat", "embedding", "asr"]).optional(),
16888
+ modelId: z9.string().trim().min(1).max(512).optional()
16705
16889
  }).strict();
16706
16890
 
16707
- // src/controllers/tenant-scope.ts
16708
- var TenantScopeError2 = class extends Error {
16709
- /** Creates a tenant scope error. */
16710
- constructor(code, statusCode, message) {
16711
- super(message);
16712
- this.code = code;
16713
- this.statusCode = statusCode;
16714
- this.name = "TenantScopeError";
16715
- }
16716
- };
16717
- function resolveTenantScope(request, authRequired2) {
16718
- const authenticated = request.user?.tenantId;
16719
- const header2 = request.headers["x-tenant-id"];
16720
- const requested = Array.isArray(header2) ? header2[0] : header2;
16721
- if (authenticated) {
16722
- if (requested && requested !== authenticated) {
16723
- throw new TenantScopeError2(
16724
- "TENANT_ACCESS_DENIED",
16725
- 403,
16726
- "Tenant access denied"
16727
- );
16728
- }
16729
- return { tenantId: authenticated, userId: request.user?.id };
16730
- }
16731
- if (authRequired2) {
16732
- throw new TenantScopeError2(
16733
- "UNAUTHENTICATED",
16734
- 401,
16735
- "Console session required"
16736
- );
16737
- }
16738
- return { tenantId: requested || "default" };
16739
- }
16740
-
16741
16891
  // src/controllers/model-providers.ts
16742
16892
  var logger4 = new Logger({ serviceName: "lattice/gateway/model-providers" });
16743
16893
  function sendError2(reply, error) {
@@ -23094,7 +23244,13 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
23094
23244
  config: lattice.client.getConfig()
23095
23245
  }))
23096
23246
  ),
23097
- forgetTenantDefaultMiss: (tenantId) => modelLatticeManager3.forgetTenantDefaultMiss(tenantId)
23247
+ forgetTenantDefaultMiss: (tenantId) => modelLatticeManager3.forgetTenantDefaultMiss(tenantId),
23248
+ evictProviderCache: (tenantId, providerId2) => {
23249
+ modelLatticeManager3.evictProviderCache(tenantId, providerId2);
23250
+ embeddingsLatticeManager2.evictProviderCache(tenantId, providerId2);
23251
+ sttModelLatticeManager2.evictProviderCache(tenantId, providerId2);
23252
+ vectorStoreLatticeManager.clearTenantVectorStores(tenantId);
23253
+ }
23098
23254
  },
23099
23255
  onChanged: (tenantId, providerId2) => broadcastModelProviderChanged(tenantId, providerId2)
23100
23256
  }, { authRequired: process.env.AUTH_REQUIRED === "true" });
@@ -25153,7 +25309,7 @@ import { setBindingRegistry, setMenuRegistry, setEvalRunService } from "@axiom-l
25153
25309
 
25154
25310
  // src/channels/wecom/WecomChannelAdapter.ts
25155
25311
  import { randomUUID as randomUUID25 } from "crypto";
25156
- import { z as z12 } from "zod";
25312
+ import { z as z13 } from "zod";
25157
25313
  import * as WecomAiBot from "@wecom/aibot-node-sdk";
25158
25314
 
25159
25315
  // src/channels/wecom/parser.ts
@@ -25178,23 +25334,287 @@ function parseWecomTextFrame(frame) {
25178
25334
  };
25179
25335
  }
25180
25336
 
25337
+ // src/channels/wecom/cardTool.ts
25338
+ import { getToolLattice, registerToolLattice } from "@axiom-lattice/core";
25339
+ import { z as z12 } from "zod";
25340
+ var CARD_TTL_MS = 10 * 6e4;
25341
+ var MAX_CARD_ENTRIES = 1e3;
25342
+ var cardEntries = /* @__PURE__ */ new Map();
25343
+ function pruneCards(now) {
25344
+ for (const [key, entry] of cardEntries) {
25345
+ if (entry.expiresAt <= now) cardEntries.delete(key);
25346
+ }
25347
+ while (cardEntries.size > MAX_CARD_ENTRIES) {
25348
+ const oldest = cardEntries.keys().next().value;
25349
+ if (oldest === void 0) break;
25350
+ cardEntries.delete(oldest);
25351
+ }
25352
+ }
25353
+ function putWecomCard(key, card) {
25354
+ const now = Date.now();
25355
+ pruneCards(now);
25356
+ if (cardEntries.get(key)?.closed) return false;
25357
+ cardEntries.delete(key);
25358
+ cardEntries.set(key, { card, expiresAt: now + CARD_TTL_MS, closed: false });
25359
+ return true;
25360
+ }
25361
+ function takeWecomCard(key) {
25362
+ const now = Date.now();
25363
+ pruneCards(now);
25364
+ const entry = cardEntries.get(key);
25365
+ if (!entry) return void 0;
25366
+ cardEntries.delete(key);
25367
+ cardEntries.set(key, { expiresAt: now + CARD_TTL_MS, closed: true });
25368
+ return entry.card;
25369
+ }
25370
+ function discardWecomCard(key) {
25371
+ const now = Date.now();
25372
+ pruneCards(now);
25373
+ const entry = cardEntries.get(key);
25374
+ if (entry) entry.card = void 0;
25375
+ cardEntries.delete(key);
25376
+ cardEntries.set(key, { expiresAt: now + CARD_TTL_MS, closed: true });
25377
+ }
25378
+ var CARD_TYPES = [
25379
+ "text_notice",
25380
+ "news_notice",
25381
+ "button_interaction",
25382
+ "vote_interaction",
25383
+ "multiple_interaction"
25384
+ ];
25385
+ var optionSchema = z12.object({
25386
+ id: z12.string().describe("\u9009\u9879 ID"),
25387
+ text: z12.string().describe("\u9009\u9879\u6587\u6848"),
25388
+ is_checked: z12.boolean().optional().describe("\u662F\u5426\u9ED8\u8BA4\u9009\u4E2D")
25389
+ });
25390
+ var schema = z12.object({
25391
+ card_type: z12.enum(CARD_TYPES).default("text_notice").describe(
25392
+ "\u5361\u7247\u7C7B\u578B\uFF1Atext_notice(\u6587\u672C\u901A\u77E5)/news_notice(\u56FE\u6587)/button_interaction(\u6309\u94AE)/vote_interaction(\u6295\u7968)/multiple_interaction(\u591A\u9009)"
25393
+ ),
25394
+ title: z12.string().min(1).max(26).describe("\u4E00\u7EA7\u6807\u9898\uFF0C\u4E0D\u8D85\u8FC7 26 \u5B57"),
25395
+ desc: z12.string().max(30).optional().describe("\u6807\u9898\u8F85\u52A9\u4FE1\u606F\uFF0C\u4E0D\u8D85\u8FC7 30 \u5B57"),
25396
+ source_desc: z12.string().max(13).optional().describe("\u5361\u7247\u5DE6\u4E0A\u89D2\u6765\u6E90\u6587\u6848\uFF0C\u4E0D\u8D85\u8FC7 13 \u5B57"),
25397
+ source_color: z12.number().int().min(0).max(3).optional().describe("\u6765\u6E90\u6587\u5B57\u989C\u8272\uFF1A0 \u7070(\u9ED8\u8BA4)/1 \u9ED1/2 \u7EA2/3 \u7EFF"),
25398
+ emphasis_title: z12.string().max(10).optional().describe("\u5173\u952E\u6570\u5B57\uFF0C\u4E0D\u8D85\u8FC7 10 \u5B57"),
25399
+ emphasis_desc: z12.string().max(15).optional().describe("\u5173\u952E\u6570\u5B57\u8BF4\u660E\uFF0C\u4E0D\u8D85\u8FC7 15 \u5B57"),
25400
+ subtitle: z12.string().max(112).optional().describe("\u4E8C\u7EA7\u666E\u901A\u6587\u672C\uFF0C\u4E0D\u8D85\u8FC7 112 \u5B57"),
25401
+ quote_text: z12.string().optional().describe("\u5F15\u7528\u6837\u5F0F\u6587\u672C\uFF08\u4E0E\u5173\u952E\u6570\u5B57\u4E8C\u9009\u4E00\uFF09"),
25402
+ items: z12.array(z12.object({
25403
+ key: z12.string().max(5).describe("\u4E8C\u7EA7\u6807\u9898\uFF0C\u4E0D\u8D85\u8FC7 5 \u5B57"),
25404
+ value: z12.string().max(26).describe("\u4E8C\u7EA7\u6587\u672C\uFF0C\u4E0D\u8D85\u8FC7 26 \u5B57"),
25405
+ url: z12.string().optional().describe("\u70B9\u51FB\u8DF3\u8F6C\u94FE\u63A5\uFF08\u53EF\u9009\uFF09")
25406
+ })).max(6).optional().describe("\u952E\u503C\u660E\u7EC6\u5217\u8868\uFF0C\u6700\u591A 6 \u6761"),
25407
+ links: z12.array(z12.object({
25408
+ title: z12.string().max(13).describe("\u94FE\u63A5\u6587\u6848\uFF0C\u4E0D\u8D85\u8FC7 13 \u5B57"),
25409
+ url: z12.string()
25410
+ })).max(3).optional().describe("\u8DF3\u8F6C\u94FE\u63A5\u5217\u8868\uFF0C\u6700\u591A 3 \u6761"),
25411
+ action_url: z12.string().optional().describe("\u6574\u5F20\u5361\u7247\u70B9\u51FB\u8DF3\u8F6C\u7684\u94FE\u63A5\uFF08\u4F01\u5FAE\u8981\u6C42\u5FC5\u586B\uFF1B\u7F3A\u7701\u65F6\u81EA\u52A8\u53D6\u7B2C\u4E00\u6761 links[].url\uFF09"),
25412
+ // ── news_notice ──────────────────────────────────────────────
25413
+ image_url: z12.string().optional().describe("\u5361\u7247\u56FE\u7247\uFF08news_notice\uFF09"),
25414
+ image_text: z12.object({ title: z12.string(), desc: z12.string().optional(), url: z12.string().optional() }).optional().describe("\u5DE6\u56FE\u53F3\u6587\u533A\u57DF\uFF08news_notice\uFF09"),
25415
+ vertical_items: z12.array(z12.object({ title: z12.string(), desc: z12.string().optional() })).max(4).optional().describe("\u5782\u76F4\u5185\u5BB9\u5217\u8868\uFF0C\u6700\u591A 4 \u6761\uFF08news_notice\uFF09"),
25416
+ // ── button_interaction ───────────────────────────────────────
25417
+ buttons: z12.array(z12.object({
25418
+ text: z12.string().max(4).describe("\u6309\u94AE\u6587\u6848\uFF0C\u4E0D\u8D85\u8FC7 4 \u5B57"),
25419
+ key: z12.string().describe("\u6309\u94AE key\uFF0C\u7528\u4E8E\u56DE\u8C03"),
25420
+ style: z12.number().int().min(1).max(4).optional().describe("\u6309\u94AE\u6837\u5F0F 1-4")
25421
+ })).max(6).optional().describe("\u6309\u94AE\u5217\u8868\uFF0C\u6700\u591A 6 \u6761\uFF08button_interaction\uFF09"),
25422
+ // ── vote_interaction ─────────────────────────────────────────
25423
+ checkbox: z12.object({
25424
+ question_key: z12.string().describe("\u9009\u62E9\u9898 key"),
25425
+ option_list: z12.array(optionSchema).max(20).describe("\u9009\u9879\u5217\u8868\uFF0C\u6700\u591A 20 \u4E2A")
25426
+ }).optional().describe("\u9009\u62E9\u9898\uFF08vote_interaction\uFF09"),
25427
+ submit_text: z12.string().optional().describe("\u63D0\u4EA4\u6309\u94AE\u6587\u6848\uFF0C\u9ED8\u8BA4\u300C\u63D0\u4EA4\u300D"),
25428
+ // ── multiple_interaction ─────────────────────────────────────
25429
+ selections: z12.array(z12.object({
25430
+ question_key: z12.string(),
25431
+ title: z12.string().optional(),
25432
+ option_list: z12.array(optionSchema).max(20)
25433
+ })).max(3).optional().describe("\u4E0B\u62C9\u9009\u62E9\u5668\u5217\u8868\uFF0C\u6700\u591A 3 \u4E2A\uFF08multiple_interaction\uFF09")
25434
+ });
25435
+ function assertUrl(url, field) {
25436
+ if (url === void 0) return;
25437
+ if (!/^https?:\/\/\S+$/i.test(url)) {
25438
+ throw new Error(`${field} \u5FC5\u987B\u662F http(s):// \u5F00\u5934\u7684\u53EF\u8BBF\u95EE\u94FE\u63A5\uFF0C\u6536\u5230\u300C${url}\u300D`);
25439
+ }
25440
+ }
25441
+ function newTaskId() {
25442
+ return `wecom-card-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
25443
+ }
25444
+ function buildTemplateCard(input) {
25445
+ if (!input.title.trim()) throw new Error("title \u4E0D\u80FD\u4E3A\u7A7A");
25446
+ assertUrl(input.action_url, "action_url");
25447
+ assertUrl(input.image_url, "image_url");
25448
+ input.links?.forEach((link, i) => assertUrl(link.url, `links[${i}].url`));
25449
+ input.items?.forEach((item, i) => assertUrl(item.url, `items[${i}].url`));
25450
+ assertUrl(input.image_text?.url, "image_text.url");
25451
+ const actionUrl = input.action_url ?? input.links?.[0]?.url;
25452
+ if (!actionUrl) {
25453
+ throw new Error(
25454
+ "\u5361\u7247\u5FC5\u987B\u63D0\u4F9B action_url\uFF08\u6574\u5361\u70B9\u51FB\u8DF3\u8F6C\uFF09\uFF0C\u6216\u81F3\u5C11\u4E00\u6761\u5E26 url \u7684 links \u2014\u2014 \u4F01\u5FAE\u8981\u6C42 card_action \u4E0D\u80FD\u4E3A\u7A7A"
25455
+ );
25456
+ }
25457
+ const card = {
25458
+ card_type: input.card_type,
25459
+ main_title: {
25460
+ title: input.title,
25461
+ ...input.desc ? { desc: input.desc } : {}
25462
+ },
25463
+ card_action: { type: 1, url: actionUrl }
25464
+ };
25465
+ if (input.source_desc) {
25466
+ card.source = {
25467
+ desc: input.source_desc,
25468
+ ...input.source_color !== void 0 ? { desc_color: input.source_color } : {}
25469
+ };
25470
+ }
25471
+ if (input.emphasis_title || input.emphasis_desc) {
25472
+ card.emphasis_content = {
25473
+ ...input.emphasis_title ? { title: input.emphasis_title } : {},
25474
+ ...input.emphasis_desc ? { desc: input.emphasis_desc } : {}
25475
+ };
25476
+ }
25477
+ if (input.subtitle) card.sub_title_text = input.subtitle;
25478
+ if (input.quote_text) card.quote_area = { type: 0, quote_text: input.quote_text };
25479
+ if (input.items?.length) {
25480
+ card.horizontal_content_list = input.items.map((item) => ({
25481
+ type: item.url ? 1 : 0,
25482
+ keyname: item.key,
25483
+ value: item.value,
25484
+ ...item.url ? { url: item.url } : {}
25485
+ }));
25486
+ }
25487
+ if (input.links?.length) {
25488
+ card.jump_list = input.links.map((link) => ({
25489
+ type: 1,
25490
+ url: link.url,
25491
+ title: link.title
25492
+ }));
25493
+ }
25494
+ if (input.card_type === "news_notice") {
25495
+ if (input.image_url) card.card_image = { url: input.image_url };
25496
+ if (input.image_text) {
25497
+ card.image_text_area = {
25498
+ type: input.image_text.url ? 1 : 0,
25499
+ ...input.image_text.url ? { url: input.image_text.url } : {},
25500
+ title: input.image_text.title,
25501
+ ...input.image_text.desc ? { desc: input.image_text.desc } : {}
25502
+ };
25503
+ }
25504
+ if (input.vertical_items?.length) {
25505
+ card.vertical_content_list = input.vertical_items.map((item) => ({
25506
+ title: item.title,
25507
+ ...item.desc ? { desc: item.desc } : {}
25508
+ }));
25509
+ }
25510
+ }
25511
+ if (input.card_type === "button_interaction") {
25512
+ if (!input.buttons?.length) throw new Error("button_interaction \u5FC5\u987B\u63D0\u4F9B buttons");
25513
+ card.button_list = input.buttons.map((button) => ({
25514
+ text: button.text,
25515
+ key: button.key,
25516
+ ...button.style !== void 0 ? { style: button.style } : {}
25517
+ }));
25518
+ card.task_id = newTaskId();
25519
+ }
25520
+ if (input.card_type === "vote_interaction") {
25521
+ if (!input.checkbox) throw new Error("vote_interaction \u5FC5\u987B\u63D0\u4F9B checkbox");
25522
+ card.checkbox = {
25523
+ question_key: input.checkbox.question_key,
25524
+ option_list: input.checkbox.option_list.map((option) => ({
25525
+ id: option.id,
25526
+ text: option.text,
25527
+ ...option.is_checked !== void 0 ? { is_checked: option.is_checked } : {}
25528
+ }))
25529
+ };
25530
+ card.submit_button = { text: input.submit_text ?? "\u63D0\u4EA4", type: 1, key: "submit" };
25531
+ card.task_id = newTaskId();
25532
+ }
25533
+ if (input.card_type === "multiple_interaction") {
25534
+ if (!input.selections?.length) throw new Error("multiple_interaction \u5FC5\u987B\u63D0\u4F9B selections");
25535
+ card.select_list = input.selections.map((selection) => ({
25536
+ question_key: selection.question_key,
25537
+ ...selection.title ? { title: selection.title } : {},
25538
+ option_list: selection.option_list.map((option) => ({
25539
+ id: option.id,
25540
+ text: option.text,
25541
+ ...option.is_checked !== void 0 ? { is_checked: option.is_checked } : {}
25542
+ }))
25543
+ }));
25544
+ card.submit_button = { text: input.submit_text ?? "\u63D0\u4EA4", type: 1, key: "submit" };
25545
+ card.task_id = newTaskId();
25546
+ }
25547
+ return card;
25548
+ }
25549
+ function readReplyKey(exeConfig) {
25550
+ const configurable = exeConfig?.configurable;
25551
+ const key = configurable?.runConfig?.wecomReplyKey;
25552
+ return typeof key === "string" && key.length > 0 ? key : void 0;
25553
+ }
25554
+ function registerWecomCardTool() {
25555
+ if (getToolLattice("wecom_card")) return;
25556
+ registerToolLattice(
25557
+ "wecom_card",
25558
+ {
25559
+ name: "wecom_card",
25560
+ description: "\u5728**\u4F01\u4E1A\u5FAE\u4FE1\u4F1A\u8BDD**\u4E2D\uFF0C\u628A\u672C\u8F6E\u56DE\u590D\u7684\u6838\u5FC3\u7ED3\u679C\u505A\u6210\u4E00\u5F20\u6A21\u677F\u5361\u7247\uFF0C\u5361\u7247\u4F1A\u5728\u672C\u6761\u56DE\u590D\u540E\u7531\u6E20\u9053\u81EA\u52A8\u53D1\u9001\u3002\n\n\u4E3B\u52A8\u4F7F\u7528\uFF08\u7528\u6237\u4E0D\u9700\u8981\u8BF4\u300C\u5361\u7247\u300D\u300C\u5361\u7247\u6D88\u606F\u300D\uFF09\uFF1A\u5F53\u56DE\u590D\u7684\u4E3B\u8981\u4EF7\u503C\u662F\u7ED3\u6784\u5316\u4E1A\u52A1\u5BF9\u8C61\u65F6 \u2014\u2014 \u5BA2\u6237\u4EA4\u63A5/\u6D3E\u5355\u3001\u544A\u8B66/\u5F02\u5E38/\u98CE\u9669\u3001\u6E05\u5355/\u6838\u5BF9\u8868\u3001\u5F85\u529E/\u884C\u52A8\u9879\u3001\u72B6\u6001/\u8FDB\u5EA6/\u6307\u6807\u6C47\u603B\u3001\u6295\u7968/\u9009\u62E9/\u786E\u8BA4\u3001\u9700\u8981\u70B9\u51FB\u8FDB\u5165\u7684\u94FE\u63A5/\u516C\u544A\u5165\u53E3\u3002\u53EA\u8981\u7ED3\u679C\u5177\u5907\u300C\u6807\u9898 + 1-6 \u6761\u952E\u503C\u660E\u7EC6\uFF08\u6216\u9009\u9879\uFF09+ \u4E00\u4E2A\u53EF\u8DF3\u8F6C\u94FE\u63A5\u300D\uFF0C\u5C31\u5E94\u8C03\u7528\u3002\n\n\u4E0D\u8981\u4F7F\u7528\uFF1A\u666E\u901A\u95EE\u7B54\u3001\u89E3\u91CA\u3001\u95F2\u804A\u3001\u957F\u6587\u5206\u6790\u3001\u4EE3\u7801\u3001\u6CA1\u6709\u660E\u786E\u8DF3\u8F6C\u76EE\u6807\u7684\u5BF9\u8BDD\u3001\u4E00\u53E5\u8BDD\u5C31\u80FD\u8BF4\u6E05\u7684\u7ED3\u679C\u3002\n\n\u8C03\u7528\u540E\u4ECD\u7528\u4E00\u4E24\u53E5\u8BDD\u603B\u7ED3\u7ED3\u8BBA\uFF1B\u4E0D\u8981\u518D\u8F93\u51FA JSON\uFF0C\u4E5F\u4E0D\u8981\u5199\u3010\u5361\u7247\u6D88\u606F\u3011\u3002\u6BCF\u6761\u56DE\u590D\u6700\u591A\u4E00\u5F20\u5361\u7247\uFF0C\u91CD\u590D\u8C03\u7528\u4F1A\u66FF\u6362\u4E0A\u4E00\u5F20\u3002\u8FD4\u56DE ERROR \u65F6\u6309\u63D0\u793A\u4FEE\u6B63\u540E\u91CD\u8BD5\uFF0C\u4E0D\u8981\u628A ERROR \u653E\u8FDB\u56DE\u590D\u3002\u5B50 agent \u4E0D\u8981\u8C03\u7528\u672C\u5DE5\u5177\u3002",
25561
+ schema
25562
+ },
25563
+ async (input, exeConfig) => {
25564
+ const replyKey = readReplyKey(exeConfig);
25565
+ if (!replyKey) {
25566
+ return "ERROR: wecom_card \u53EA\u80FD\u5728\u4F01\u4E1A\u5FAE\u4FE1\u5B9E\u65F6\u4F1A\u8BDD\u4E2D\u4F7F\u7528\uFF0C\u8BF7\u76F4\u63A5\u7528\u6587\u5B57\u56DE\u590D\u3002";
25567
+ }
25568
+ try {
25569
+ const card = buildTemplateCard(input);
25570
+ if (!putWecomCard(replyKey, card)) {
25571
+ return "ERROR: \u672C\u6761\u56DE\u590D\u5DF2\u7ED3\u675F\uFF0C\u65E0\u6CD5\u518D\u9644\u5E26\u5361\u7247\u3002\u8BF7\u628A\u7ED3\u679C\u5199\u5165\u6B63\u6587\u3002";
25572
+ }
25573
+ const title = card.main_title?.title;
25574
+ const suffix = typeof title === "string" && title ? `\u300C${title}\u300D` : "";
25575
+ return `\u5DF2\u751F\u6210\u4F01\u4E1A\u5FAE\u4FE1\u5361\u7247${suffix}\uFF0C\u5C06\u968F\u672C\u6761\u56DE\u590D\u81EA\u52A8\u53D1\u9001\u3002\u8BF7\u7528\u6587\u5B57\u603B\u7ED3\u7ED3\u8BBA\uFF0C\u4E0D\u8981\u91CD\u590D\u5361\u7247\u5185\u5BB9\u3002`;
25576
+ } catch (error) {
25577
+ return `ERROR: ${error instanceof Error ? error.message : String(error)}\u3002\u8BF7\u4FEE\u6B63\u53C2\u6570\u540E\u91CD\u65B0\u8C03\u7528 wecom_card\u3002`;
25578
+ }
25579
+ }
25580
+ );
25581
+ }
25582
+
25181
25583
  // src/channels/wecom/WecomChannelAdapter.ts
25182
25584
  var logger6 = new Logger({ serviceName: "lattice/gateway/wecom" });
25183
- var wecomConfigSchema = z12.object({
25184
- botId: z12.string(),
25185
- secret: z12.string()
25585
+ var wecomConfigSchema = z13.object({
25586
+ botId: z13.string(),
25587
+ secret: z13.string()
25186
25588
  });
25187
25589
  var RECONNECT_BASE_MS = 5e3;
25188
25590
  var RECONNECT_MAX_MS = 6e4;
25591
+ var SENT_CARD_TTL_MS = 10 * 6e4;
25592
+ var MAX_SENT_CARDS = 1e3;
25189
25593
  var activeConnections2 = /* @__PURE__ */ new Map();
25190
25594
  var streamIdsByReqId = /* @__PURE__ */ new Map();
25191
25595
  var MAX_TRACKED_STREAMS = 1e3;
25192
- function streamKeyOf(replyTarget) {
25193
- const frame = replyTarget.rawTarget.frame;
25194
- return frame?.headers?.req_id ?? replyTarget.channelInstallationId;
25596
+ var sentCardsByTaskId = /* @__PURE__ */ new Map();
25597
+ var handledCardEventKeys = /* @__PURE__ */ new Map();
25598
+ var ACKNOWLEDGED_TTL_MS = 10 * 6e4;
25599
+ function isRecord4(value) {
25600
+ return value !== null && typeof value === "object" && !Array.isArray(value);
25601
+ }
25602
+ function replyKeyOf(installationId, reqId) {
25603
+ return `${installationId}:${reqId ?? "no-req"}`;
25604
+ }
25605
+ function replyKeyForTarget(replyTarget) {
25606
+ const frame = replyTarget.rawTarget?.frame;
25607
+ return replyKeyOf(replyTarget.channelInstallationId, frame?.headers?.req_id);
25608
+ }
25609
+ function isEventReplyTarget(replyTarget) {
25610
+ return replyTarget.rawTarget?.kind === "template_card_event";
25611
+ }
25612
+ function eventChatId(replyTarget) {
25613
+ const chatId = replyTarget.rawTarget?.chatId;
25614
+ return typeof chatId === "string" && chatId.length > 0 ? chatId : void 0;
25195
25615
  }
25196
25616
  function streamIdFor(replyTarget) {
25197
- const key = streamKeyOf(replyTarget);
25617
+ const key = replyKeyForTarget(replyTarget);
25198
25618
  let streamId = streamIdsByReqId.get(key);
25199
25619
  if (!streamId) {
25200
25620
  streamId = randomUUID25();
@@ -25206,6 +25626,267 @@ function streamIdFor(replyTarget) {
25206
25626
  }
25207
25627
  return streamId;
25208
25628
  }
25629
+ function rememberSentCard(card) {
25630
+ const taskId = typeof card.task_id === "string" ? card.task_id : void 0;
25631
+ if (!taskId) return;
25632
+ const now = Date.now();
25633
+ for (const [key, entry] of sentCardsByTaskId) {
25634
+ if (entry.expiresAt <= now) sentCardsByTaskId.delete(key);
25635
+ }
25636
+ sentCardsByTaskId.set(taskId, { card, expiresAt: now + SENT_CARD_TTL_MS });
25637
+ while (sentCardsByTaskId.size > MAX_SENT_CARDS) {
25638
+ const oldest = sentCardsByTaskId.keys().next().value;
25639
+ if (oldest === void 0) break;
25640
+ sentCardsByTaskId.delete(oldest);
25641
+ }
25642
+ }
25643
+ function sentCardForTask(taskId) {
25644
+ if (!taskId) return void 0;
25645
+ const entry = sentCardsByTaskId.get(taskId);
25646
+ if (!entry) return void 0;
25647
+ if (entry.expiresAt <= Date.now()) {
25648
+ sentCardsByTaskId.delete(taskId);
25649
+ return void 0;
25650
+ }
25651
+ return entry.card;
25652
+ }
25653
+ function claimCardEvent(taskId, eventKey) {
25654
+ if (!taskId) return true;
25655
+ const key = `${taskId}:${eventKey ?? ""}`;
25656
+ const now = Date.now();
25657
+ for (const [handled, expiresAt] of handledCardEventKeys) {
25658
+ if (expiresAt <= now) handledCardEventKeys.delete(handled);
25659
+ }
25660
+ if (handledCardEventKeys.has(key)) return false;
25661
+ handledCardEventKeys.set(key, now + ACKNOWLEDGED_TTL_MS);
25662
+ while (handledCardEventKeys.size > MAX_SENT_CARDS) {
25663
+ const oldest = handledCardEventKeys.keys().next().value;
25664
+ if (oldest === void 0) break;
25665
+ handledCardEventKeys.delete(oldest);
25666
+ }
25667
+ return true;
25668
+ }
25669
+ async function sendStreamFrame(connection, replyTarget, input) {
25670
+ if (isEventReplyTarget(replyTarget)) {
25671
+ await sendEventReply(connection, replyTarget, input);
25672
+ return;
25673
+ }
25674
+ const frame = replyTarget.rawTarget?.frame;
25675
+ if (!frame) {
25676
+ logger6.warn("WeCom reply target has no inbound frame, skipping", {
25677
+ installationId: replyTarget.channelInstallationId
25678
+ });
25679
+ return;
25680
+ }
25681
+ const report = (message, error) => logger6.warn(message, {
25682
+ installationId: replyTarget.channelInstallationId,
25683
+ error: error instanceof Error ? error.message : String(error)
25684
+ });
25685
+ const text = input.text || (input.done ? input.card ? "\u5DF2\u5B8C\u6210\uFF0C\u8BE6\u60C5\u89C1\u4E0B\u65B9\u5361\u7247\u3002" : "\u5904\u7406\u5B8C\u6210\u3002" : "");
25686
+ if (!text) return;
25687
+ try {
25688
+ await Promise.resolve(connection.client.reply(frame, {
25689
+ msgtype: "stream",
25690
+ stream: { id: streamIdFor(replyTarget), finish: input.done, content: text }
25691
+ }));
25692
+ } catch (error) {
25693
+ report("WeCom stream reply failed", error);
25694
+ }
25695
+ if (input.done && input.card) {
25696
+ try {
25697
+ await Promise.resolve(connection.client.reply(frame, {
25698
+ msgtype: "template_card",
25699
+ template_card: input.card
25700
+ }));
25701
+ rememberSentCard(input.card);
25702
+ logger6.info("WeCom card sent", {
25703
+ installationId: replyTarget.channelInstallationId,
25704
+ cardType: input.card.card_type
25705
+ });
25706
+ } catch (error) {
25707
+ report("WeCom card reply failed (text already sent)", error);
25708
+ }
25709
+ }
25710
+ }
25711
+ async function sendEventReply(connection, replyTarget, input) {
25712
+ if (!input.done) return;
25713
+ const chatId = eventChatId(replyTarget);
25714
+ if (!chatId) {
25715
+ logger6.warn("WeCom card event has no chat id, cannot reply", {
25716
+ installationId: replyTarget.channelInstallationId
25717
+ });
25718
+ return;
25719
+ }
25720
+ const sendMessage = connection.client.sendMessage;
25721
+ if (!sendMessage) {
25722
+ logger6.warn("WeCom client does not support sendMessage, cannot reply to card event", {
25723
+ installationId: replyTarget.channelInstallationId
25724
+ });
25725
+ return;
25726
+ }
25727
+ const report = (message, error) => logger6.warn(message, {
25728
+ installationId: replyTarget.channelInstallationId,
25729
+ error: error instanceof Error ? error.message : String(error)
25730
+ });
25731
+ if (input.card) {
25732
+ try {
25733
+ await Promise.resolve(sendMessage.call(connection.client, chatId, {
25734
+ msgtype: "template_card",
25735
+ template_card: input.card
25736
+ }));
25737
+ rememberSentCard(input.card);
25738
+ logger6.info("WeCom card sent", {
25739
+ installationId: replyTarget.channelInstallationId,
25740
+ cardType: input.card.card_type
25741
+ });
25742
+ } catch (error) {
25743
+ report("WeCom card event card failed", error);
25744
+ }
25745
+ return;
25746
+ }
25747
+ if (input.text) {
25748
+ try {
25749
+ await Promise.resolve(sendMessage.call(connection.client, chatId, {
25750
+ msgtype: "markdown",
25751
+ markdown: { content: input.text }
25752
+ }));
25753
+ } catch (error) {
25754
+ report("WeCom card event reply failed", error);
25755
+ }
25756
+ }
25757
+ }
25758
+ function acknowledgedCard(taskId, eventKey) {
25759
+ const desc = eventKey ? `\u5DF2\u6536\u5230\uFF1A${eventKey}` : "\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u5904\u7406\u2026";
25760
+ const original = sentCardForTask(taskId);
25761
+ if (original) {
25762
+ const mainTitle = isRecord4(original.main_title) ? original.main_title : {};
25763
+ return { ...original, main_title: { ...mainTitle, desc } };
25764
+ }
25765
+ return {
25766
+ card_type: "text_notice",
25767
+ main_title: { title: "\u5DF2\u6536\u5230\u4F60\u7684\u64CD\u4F5C", desc },
25768
+ ...taskId ? { task_id: taskId } : {}
25769
+ };
25770
+ }
25771
+ function cardEventFields(event) {
25772
+ const nested = isRecord4(event?.template_card_event) ? event.template_card_event : event;
25773
+ const source = nested ?? {};
25774
+ return {
25775
+ cardEvent: source,
25776
+ taskId: typeof source.task_id === "string" ? source.task_id : void 0,
25777
+ eventKey: typeof source.event_key === "string" ? source.event_key : void 0,
25778
+ cardType: typeof source.card_type === "string" ? source.card_type : void 0
25779
+ };
25780
+ }
25781
+ function describeError(error) {
25782
+ if (error instanceof Error) return error.message;
25783
+ if (typeof error === "string") return error;
25784
+ try {
25785
+ return JSON.stringify(error);
25786
+ } catch {
25787
+ return String(error);
25788
+ }
25789
+ }
25790
+ function cardEventInbound(frame, installation, input) {
25791
+ const body = frame.body;
25792
+ const { cardEvent, taskId, eventKey, cardType } = cardEventFields(body.event);
25793
+ const summary = eventKey ? `\u7528\u6237\u70B9\u51FB\u4E86\u5361\u7247\u6309\u94AE\u300C${eventKey}\u300D${taskId ? `\uFF08task_id=${taskId}\uFF09` : ""}` : `\u7528\u6237\u63D0\u4EA4\u4E86\u5361\u7247\u64CD\u4F5C${taskId ? `\uFF08task_id=${taskId}\uFF09` : ""}\uFF1A${JSON.stringify(cardEvent)}`;
25794
+ return {
25795
+ channel: "wecom",
25796
+ channelInstallationId: installation.id,
25797
+ tenantId: installation.tenantId,
25798
+ sender: { id: input.userId },
25799
+ content: {
25800
+ text: summary,
25801
+ metadata: {
25802
+ messageId: body.msgid,
25803
+ chatId: input.chatId,
25804
+ chatType: body.chattype === "group" ? "group" : "direct",
25805
+ taskId,
25806
+ eventKey,
25807
+ cardType,
25808
+ cardEvent,
25809
+ wecomReplyKey: replyKeyOf(installation.id, frame.headers.req_id)
25810
+ }
25811
+ },
25812
+ conversation: {
25813
+ id: input.chatId,
25814
+ type: body.chattype === "group" ? "group" : "direct"
25815
+ },
25816
+ replyTarget: {
25817
+ adapterChannel: "wecom",
25818
+ channelInstallationId: installation.id,
25819
+ rawTarget: { kind: "template_card_event", frame, chatId: input.chatId }
25820
+ }
25821
+ };
25822
+ }
25823
+ function handleTemplateCardEvent(rawFrame, installation, client, router) {
25824
+ const frame = rawFrame;
25825
+ const body = frame?.body;
25826
+ const event = body?.event;
25827
+ const reqId = frame?.headers?.req_id;
25828
+ if (!body || event?.eventtype !== "template_card_event" || typeof reqId !== "string") {
25829
+ return;
25830
+ }
25831
+ const userId = body.from?.userid;
25832
+ const chatId = typeof body.chatid === "string" && body.chatid ? body.chatid : userId;
25833
+ if (!userId || !chatId) {
25834
+ logger6.warn("WeCom card event missing user/chat id, skipping", {
25835
+ installationId: installation.id
25836
+ });
25837
+ return;
25838
+ }
25839
+ const { taskId, eventKey } = cardEventFields(event);
25840
+ if (!claimCardEvent(taskId, eventKey)) {
25841
+ logger6.info("WeCom duplicate card event ignored", {
25842
+ installationId: installation.id,
25843
+ taskId,
25844
+ eventKey
25845
+ });
25846
+ return;
25847
+ }
25848
+ const updateTemplateCard = client.updateTemplateCard;
25849
+ if (updateTemplateCard) {
25850
+ const updated = acknowledgedCard(taskId, eventKey);
25851
+ try {
25852
+ void Promise.resolve(updateTemplateCard.call(client, rawFrame, updated)).catch((error) => {
25853
+ logger6.warn("WeCom template card update failed", {
25854
+ installationId: installation.id,
25855
+ error: describeError(error)
25856
+ });
25857
+ });
25858
+ } catch (error) {
25859
+ logger6.warn("WeCom template card update failed", {
25860
+ installationId: installation.id,
25861
+ error: describeError(error)
25862
+ });
25863
+ }
25864
+ } else {
25865
+ logger6.warn("WeCom client does not support updateTemplateCard", {
25866
+ installationId: installation.id
25867
+ });
25868
+ }
25869
+ if (!router) return;
25870
+ logger6.info("WeCom card event dispatched", {
25871
+ installationId: installation.id,
25872
+ taskId,
25873
+ eventKey,
25874
+ chatId
25875
+ });
25876
+ try {
25877
+ void router.dispatch(cardEventInbound(frame, installation, { chatId, userId })).catch((error) => {
25878
+ logger6.error("WeCom card event dispatch error", {
25879
+ installationId: installation.id,
25880
+ error: describeError(error)
25881
+ });
25882
+ });
25883
+ } catch (error) {
25884
+ logger6.error("WeCom card event dispatch error", {
25885
+ installationId: installation.id,
25886
+ error: describeError(error)
25887
+ });
25888
+ }
25889
+ }
25209
25890
  var wecomChannelAdapter = {
25210
25891
  channel: "wecom",
25211
25892
  configSchema: wecomConfigSchema,
@@ -25219,11 +25900,13 @@ var wecomChannelAdapter = {
25219
25900
  sender: { id: parsed.fromUserId },
25220
25901
  content: {
25221
25902
  text: parsed.text,
25222
- // `messageId` 供去重中间件使用(企业微信可能重投同一 msgid)。
25903
+ // `messageId` 供去重中间件使用(企业微信可能重投同一 msgid);
25904
+ // `wecomReplyKey` 供 `wecom_card` 工具登记本轮卡片。
25223
25905
  metadata: {
25224
25906
  messageId: parsed.msgId,
25225
25907
  chatId: parsed.chatId,
25226
- chatType: parsed.chatType
25908
+ chatType: parsed.chatType,
25909
+ wecomReplyKey: replyKeyOf(installation.id, parsed.reqId)
25227
25910
  }
25228
25911
  },
25229
25912
  conversation: {
@@ -25247,14 +25930,12 @@ var wecomChannelAdapter = {
25247
25930
  });
25248
25931
  return;
25249
25932
  }
25250
- const key = streamKeyOf(replyTarget);
25251
- connection.client.reply(replyTarget.rawTarget.frame, {
25252
- msgtype: "stream",
25253
- stream: {
25254
- id: streamIdFor(replyTarget),
25255
- finish: true,
25256
- content: message.text
25257
- }
25933
+ const key = replyKeyForTarget(replyTarget);
25934
+ const card = takeWecomCard(key);
25935
+ await sendStreamFrame(connection, replyTarget, {
25936
+ text: message.text,
25937
+ done: true,
25938
+ card
25258
25939
  });
25259
25940
  streamIdsByReqId.delete(key);
25260
25941
  },
@@ -25273,15 +25954,21 @@ var wecomChannelAdapter = {
25273
25954
  });
25274
25955
  return;
25275
25956
  }
25276
- connection.client.reply(replyTarget.rawTarget.frame, {
25277
- msgtype: "stream",
25278
- stream: {
25279
- id: streamIdFor(replyTarget),
25280
- finish: chunk.done,
25281
- content: chunk.text
25957
+ const key = replyKeyForTarget(replyTarget);
25958
+ let card;
25959
+ if (chunk.done) {
25960
+ if (chunk.reason !== void 0 && chunk.reason !== "completed") {
25961
+ discardWecomCard(key);
25962
+ } else {
25963
+ card = takeWecomCard(key);
25282
25964
  }
25965
+ }
25966
+ await sendStreamFrame(connection, replyTarget, {
25967
+ text: chunk.text,
25968
+ done: chunk.done,
25969
+ card
25283
25970
  });
25284
- if (chunk.done) streamIdsByReqId.delete(streamKeyOf(replyTarget));
25971
+ if (chunk.done) streamIdsByReqId.delete(key);
25285
25972
  },
25286
25973
  resolveThreadId(message, binding) {
25287
25974
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -25315,6 +26002,9 @@ var wecomChannelAdapter = {
25315
26002
  });
25316
26003
  });
25317
26004
  });
26005
+ client.on("event.template_card_event", (frame) => {
26006
+ handleTemplateCardEvent(frame, installation, client, router);
26007
+ });
25318
26008
  client.on("authenticated", () => {
25319
26009
  connection.attempts = 0;
25320
26010
  });
@@ -25811,6 +26501,9 @@ import {
25811
26501
  agentLatticeManager as agentLatticeManager5,
25812
26502
  configurePluginCatalogSync,
25813
26503
  PLUGIN_CATALOG_CHANGED_TOPIC,
26504
+ configureCollectionSync,
26505
+ COLLECTION_CHANGED_TOPIC,
26506
+ vectorStoreLatticeManager as vectorStoreLatticeManager2,
25814
26507
  getSandboxPluginCatalog as getSandboxPluginCatalog3,
25815
26508
  getSandBoxManager as getSandBoxManager4,
25816
26509
  installPluginHost
@@ -25843,6 +26536,7 @@ var ChannelInstallationNotFoundError = class extends Error {
25843
26536
  }
25844
26537
  };
25845
26538
  var DEFAULT_STREAM_PLACEHOLDER = "\u6B63\u5728\u5904\u7406\u2026";
26539
+ var DEFAULT_STREAM_FAILURE_TEXT = "\u5904\u7406\u672A\u5B8C\u6210\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
25846
26540
  function streamPlaceholderFor(installation) {
25847
26541
  const config = installation?.config;
25848
26542
  if (config !== null && typeof config === "object" && !Array.isArray(config)) {
@@ -25851,6 +26545,14 @@ function streamPlaceholderFor(installation) {
25851
26545
  }
25852
26546
  return DEFAULT_STREAM_PLACEHOLDER;
25853
26547
  }
26548
+ function streamFailureTextFor(installation) {
26549
+ const config = installation?.config;
26550
+ if (config !== null && typeof config === "object" && !Array.isArray(config)) {
26551
+ const value = config.streamFailureText;
26552
+ if (typeof value === "string") return value;
26553
+ }
26554
+ return DEFAULT_STREAM_FAILURE_TEXT;
26555
+ }
25854
26556
  var InvalidChannelInstallationError = class extends Error {
25855
26557
  constructor(message = "Channel installation is not valid for this message") {
25856
26558
  super(message);
@@ -26262,7 +26964,13 @@ var _MessageRouter = class _MessageRouter {
26262
26964
  if (cleaned || !event.consumedInputMessageIds.includes(inputMessageId)) return;
26263
26965
  cleanup();
26264
26966
  if (event.inputMessageId !== inputMessageId) return;
26265
- const delivery = replyAdapter.sendStreamChunk ? streamState.finished ? void 0 : replyAdapter.sendStreamChunk(replyTarget, { text: event.outputText, done: true }, installation) : replyAdapter.sendReply(replyTarget, {
26967
+ if (replyAdapter.sendStreamChunk && streamState.finished) return;
26968
+ if (replyAdapter.sendStreamChunk) streamState.finished = true;
26969
+ const delivery = replyAdapter.sendStreamChunk ? replyAdapter.sendStreamChunk(
26970
+ replyTarget,
26971
+ { text: event.outputText, done: true, reason: "completed" },
26972
+ installation
26973
+ ) : replyAdapter.sendReply(replyTarget, {
26266
26974
  text: event.outputText,
26267
26975
  metadata: { inputMessageId, outputMessageId: event.outputMessageId }
26268
26976
  }, installation);
@@ -26386,17 +27094,34 @@ var _MessageRouter = class _MessageRouter {
26386
27094
  const send = adapter.sendStreamChunk;
26387
27095
  if (!send) return;
26388
27096
  let started = false;
27097
+ let normalEnd = false;
26389
27098
  let accumulated = "";
26390
27099
  let lastSentText = "";
26391
27100
  let lastSentAt = 0;
26392
27101
  let sent = 0;
26393
27102
  const warn = (event, error) => console.warn({ event, inputMessageId, error: error instanceof Error ? error.message : String(error) });
26394
- const push = (text, done) => send.call(adapter, replyTarget, { text, done }, installation).catch((error) => warn("dispatch:stream:failed", error));
27103
+ const push = (text, done, reason) => send.call(
27104
+ adapter,
27105
+ replyTarget,
27106
+ reason ? { text, done, reason } : { text, done },
27107
+ installation
27108
+ ).catch((error) => warn("dispatch:stream:failed", error));
26395
27109
  const placeholder = streamPlaceholderFor(installation);
27110
+ const closeWith = async (reason) => {
27111
+ if (state.finished) return;
27112
+ state.finished = true;
27113
+ const text = accumulated || (reason === "completed" ? "" : streamFailureTextFor(installation) || placeholder);
27114
+ await push(text, true, reason);
27115
+ };
27116
+ const timer = setTimeout(() => {
27117
+ void closeWith("timeout");
27118
+ }, _MessageRouter.STREAM_TIMEOUT_MS);
27119
+ timer.unref?.();
26396
27120
  try {
26397
27121
  if (placeholder) await push(placeholder, false);
26398
27122
  const stream = agent.chunkStream(inputMessageId, []);
26399
27123
  for await (const chunk of stream) {
27124
+ if (state.finished) break;
26400
27125
  if (!started) {
26401
27126
  if (chunk?.type === MessageChunkTypes4.HUMAN && chunk?.data?.id === inputMessageId) {
26402
27127
  started = true;
@@ -26404,12 +27129,19 @@ var _MessageRouter = class _MessageRouter {
26404
27129
  continue;
26405
27130
  }
26406
27131
  if (chunk?.type === MessageChunkTypes4.MESSAGE_COMPLETED || chunk?.type === MessageChunkTypes4.MESSAGE_FAILED || chunk?.type === MessageChunkTypes4.INTERRUPT) {
26407
- if (chunk.type !== MessageChunkTypes4.MESSAGE_COMPLETED) {
26408
- const text2 = accumulated || placeholder;
26409
- if (text2) {
26410
- await push(text2, true);
26411
- state.finished = true;
26412
- }
27132
+ if (chunk.type === MessageChunkTypes4.MESSAGE_COMPLETED) {
27133
+ normalEnd = true;
27134
+ const grace = setTimeout(
27135
+ () => {
27136
+ void closeWith("completed");
27137
+ },
27138
+ _MessageRouter.COMPLETION_GRACE_MS
27139
+ );
27140
+ grace.unref?.();
27141
+ } else if (chunk.type === MessageChunkTypes4.MESSAGE_FAILED) {
27142
+ await closeWith("failed");
27143
+ } else {
27144
+ await closeWith("interrupted");
26413
27145
  }
26414
27146
  break;
26415
27147
  }
@@ -26429,7 +27161,11 @@ var _MessageRouter = class _MessageRouter {
26429
27161
  }
26430
27162
  } catch (error) {
26431
27163
  warn("dispatch:stream:error", error);
27164
+ await closeWith("failed");
27165
+ } finally {
27166
+ clearTimeout(timer);
26432
27167
  }
27168
+ if (!normalEnd) await closeWith("failed");
26433
27169
  }
26434
27170
  /**
26435
27171
  * Projects a pending HITL interrupt into the room as an independent bot message.
@@ -26454,15 +27190,30 @@ var _MessageRouter = class _MessageRouter {
26454
27190
  }
26455
27191
  }
26456
27192
  };
26457
- /** 流式中途刷新的节流间隔与上限(微信单会话 30 条/分钟,留余量)。 */
26458
- _MessageRouter.STREAM_CHUNK_INTERVAL_MS = 800;
26459
- _MessageRouter.MAX_STREAM_CHUNKS = 60;
27193
+ /**
27194
+ * 流式中途刷新的节流间隔与上限。
27195
+ *
27196
+ * 企业微信单会话限制 30 条/分钟(含占位、收尾和卡片)。2500ms 约 24 次
27197
+ * 刷新/分钟,再加最多 3 条生命周期消息仍留有余量。
27198
+ */
27199
+ _MessageRouter.STREAM_CHUNK_INTERVAL_MS = 2500;
27200
+ _MessageRouter.MAX_STREAM_CHUNKS = 190;
27201
+ /** 流式回复的硬超时:企微要求 10 分钟内 `finish=true`,留 2 分钟余量。 */
27202
+ _MessageRouter.STREAM_TIMEOUT_MS = 8 * 6e4;
27203
+ /**
27204
+ * `MESSAGE_COMPLETED` 后等待 `reply:ready` 的宽限。
27205
+ *
27206
+ * 正常完成通常由 `reply:ready` 用权威全文收尾;但最终 AI 消息为空时
27207
+ * (例如模型只调用了 `wecom_card`、没有产出文字)不会触发 `reply:ready`,
27208
+ * 这里兜底自行收尾,避免流挂住、卡片丢失。
27209
+ */
27210
+ _MessageRouter.COMPLETION_GRACE_MS = 3e3;
26460
27211
  var MessageRouter = _MessageRouter;
26461
27212
 
26462
27213
  // src/channels/room/RoomChannelAdapter.ts
26463
- import { z as z13 } from "zod";
27214
+ import { z as z14 } from "zod";
26464
27215
  import { snapshotExactRecord as snapshotExactRecord16 } from "@axiom-lattice/protocols";
26465
- var roomConfigSchema = z13.object({}).strict();
27216
+ var roomConfigSchema = z14.object({}).strict();
26466
27217
  function createRoomChannelAdapter(writer) {
26467
27218
  return {
26468
27219
  channel: "room",
@@ -26624,7 +27375,7 @@ var PluginDeploymentError = class extends Error {
26624
27375
  this.code = code;
26625
27376
  }
26626
27377
  };
26627
- function isRecord4(value) {
27378
+ function isRecord5(value) {
26628
27379
  return typeof value === "object" && value !== null && !Array.isArray(value);
26629
27380
  }
26630
27381
  function buildBootstrapEntrypoint(pluginType) {
@@ -27178,7 +27929,7 @@ var PluginDeploymentService = class {
27178
27929
  return { ok: false, error: `health responded with HTTP ${response.status}` };
27179
27930
  }
27180
27931
  const body = await response.json().catch(() => void 0);
27181
- if (isRecord4(body) && body.ok === true) {
27932
+ if (isRecord5(body) && body.ok === true) {
27182
27933
  return { ok: true };
27183
27934
  }
27184
27935
  return { ok: false, error: "health returned an invalid envelope" };
@@ -27825,6 +28576,7 @@ var start = async (config) => {
27825
28576
  const adapterRegistry = new ChannelAdapterRegistry();
27826
28577
  adapterRegistry.register(larkChannelAdapter);
27827
28578
  adapterRegistry.register(wecomChannelAdapter);
28579
+ registerWecomCardTool();
27828
28580
  const { wechatChannelAdapter } = await import("./WechatChannelAdapter-7WHE5UGY.mjs");
27829
28581
  adapterRegistry.register(wechatChannelAdapter);
27830
28582
  try {
@@ -28059,6 +28811,7 @@ var start = async (config) => {
28059
28811
  taskLifecycleNotifier3.setOutbox(outboxStore);
28060
28812
  projectTaskAdmissionNotifier2.setOutbox(outboxStore);
28061
28813
  configurePluginCatalogSync({ outbox: outboxStore, owner: getReplicaId7() });
28814
+ configureCollectionSync({ outbox: outboxStore, owner: getReplicaId7() });
28062
28815
  bridgeAssistantEvents(
28063
28816
  (event, cb) => eventBus4.subscribe(event, cb),
28064
28817
  outboxStore,
@@ -28089,6 +28842,12 @@ var start = async (config) => {
28089
28842
  manager: mcpManager3,
28090
28843
  convertToConnection: convertToConnection2
28091
28844
  }),
28845
+ [COLLECTION_CHANGED_TOPIC]: async (payload) => {
28846
+ const tenantId = payload?.tenantId;
28847
+ if (typeof tenantId === "string" && tenantId.length > 0) {
28848
+ vectorStoreLatticeManager2.clearTenantVectorStores(tenantId);
28849
+ }
28850
+ },
28092
28851
  [PLUGIN_CATALOG_CHANGED_TOPIC]: async (payload) => {
28093
28852
  const tenantId = payload?.tenantId;
28094
28853
  if (typeof tenantId === "string" && tenantId.length > 0) {