privateboard 0.1.29 → 0.1.32
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.
- package/dist/boot.js +1324 -186
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +1324 -186
- package/dist/cli.js.map +1 -1
- package/dist/server.js +1324 -186
- package/dist/server.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +2 -1
- package/public/adjourn-overlay.css +64 -3
- package/public/agent-profile.css +50 -0
- package/public/agent-profile.js +62 -0
- package/public/app.js +731 -155
- package/public/home-3d-loader.js +4 -1
- package/public/home.html +12 -1
- package/public/i18n.js +184 -0
- package/public/index.html +296 -204
- package/public/key-validators.js +9 -0
- package/public/keys-store.js +150 -1
- package/public/onboarding.css +8 -0
- package/public/onboarding.js +57 -21
- package/public/room-settings.css +147 -0
- package/public/themes.css +37 -6
- package/public/typing-sfx.js +69 -7
- package/public/user-settings.js +793 -58
- package/public/vendor/mp4-muxer.min.js +1904 -0
- package/public/voice-3d-banner.js +21 -0
- package/public/voice-3d.js +93 -4
- package/public/voice-onboarding.css +7 -0
- package/public/voice-recorder.js +764 -0
- package/public/voice-replay.css +62 -0
- package/public/voice-replay.js +82 -11
package/dist/boot.js
CHANGED
|
@@ -856,6 +856,38 @@ CREATE INDEX idx_user_long_memory_active
|
|
|
856
856
|
}
|
|
857
857
|
});
|
|
858
858
|
|
|
859
|
+
// src/storage/migrations/049_voice_credentials.sql
|
|
860
|
+
var voice_credentials_default;
|
|
861
|
+
var init_voice_credentials = __esm({
|
|
862
|
+
"src/storage/migrations/049_voice_credentials.sql"() {
|
|
863
|
+
voice_credentials_default = "-- 049_voice_credentials.sql\n--\n-- Multi-instance VOICE TTS provider credentials \xB7 mirrors the LLM\n-- credential model that landed in 046. The same TTS provider can\n-- now be added more than once (e.g. two MiniMax accounts, or\n-- personal + team ElevenLabs) with distinct user-supplied labels.\n-- Legacy `provider_keys` was keyed by `provider` (one row per\n-- provider), which doesn't support this. The new table is keyed by\n-- an opaque `id`.\n--\n-- `prefs.active_voice_credential_id` is the single pointer that\n-- decides which TTS provider is \"active\" right now. Switching is\n-- a one-write change to this id; the credential rows themselves\n-- stay on file so the user can flip back without re-pasting.\n--\n-- Skill keys (brave, tavily) remain in `provider_keys` \u2014 they have\n-- no use case for multiple credentials per provider and their\n-- routes work fine as-is. After this migration, `provider_keys`\n-- holds ONLY skill rows.\n\nCREATE TABLE IF NOT EXISTS voice_credentials (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n label TEXT NOT NULL,\n key_blob BLOB NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS voice_credentials_provider_idx\n ON voice_credentials(provider);\n\nALTER TABLE prefs ADD COLUMN active_voice_credential_id TEXT;\n\n-- One-shot migration \xB7 move every configured voice row from\n-- provider_keys into voice_credentials. Generated id uses a\n-- 16-char hex randomblob slice for collision resistance; labels\n-- default to the provider's display name (UI surfaces them under\n-- that label until the user renames). Empty key_blobs are skipped\n-- (legacy \"ever-configured\" rows that have since been emptied).\nINSERT INTO voice_credentials (id, provider, label, key_blob, created_at, updated_at)\nSELECT\n lower(hex(randomblob(8))) AS id,\n provider,\n CASE provider\n WHEN 'minimax' THEN 'MiniMax'\n WHEN 'elevenlabs' THEN 'ElevenLabs'\n ELSE provider\n END AS label,\n key_blob,\n created_at,\n updated_at\nFROM provider_keys\nWHERE provider IN ('minimax','elevenlabs')\n AND length(key_blob) > 0;\n\n-- Seed prefs.active_voice_credential_id with the highest-priority\n-- migrated credential. Priority mirrors the existing fallback chain\n-- in src/voice/tts.ts:165-179 (MiniMax > ElevenLabs).\nUPDATE prefs\n SET active_voice_credential_id = (\n SELECT id FROM voice_credentials\n ORDER BY CASE provider\n WHEN 'minimax' THEN 1\n WHEN 'elevenlabs' THEN 2\n END,\n created_at ASC\n LIMIT 1\n )\n WHERE id = 1\n AND active_voice_credential_id IS NULL;\n\n-- Finally \xB7 remove the migrated voice rows from provider_keys so\n-- that table is purely skill keys (brave + tavily) from here on.\n-- Cleared by name; an explicit IN() list matches what we inserted\n-- above. After this migration, `getKey(\"minimax\")` and\n-- `getKey(\"elevenlabs\")` always return null; callers must read\n-- the active voice credential instead.\nDELETE FROM provider_keys\n WHERE provider IN ('minimax','elevenlabs');\n";
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// src/storage/migrations/050_agent_provider_buckets.sql
|
|
868
|
+
var agent_provider_buckets_default;
|
|
869
|
+
var init_agent_provider_buckets = __esm({
|
|
870
|
+
"src/storage/migrations/050_agent_provider_buckets.sql"() {
|
|
871
|
+
agent_provider_buckets_default = "-- 050_agent_provider_buckets.sql\n--\n-- SIM-swap memory for per-agent model + voice picks \xB7 keyed by\n-- provider. Today every `agents` row stores a single `model_v` and a\n-- single `voice_json`; switching the active LLM credential's provider\n-- runs `reconcileAgentModels({forcePrimary:true})` which overwrites\n-- both fields with random picks from the new carrier's pool \xB7 the\n-- user's manual per-director picks under the prior provider are lost.\n--\n-- This migration adds two TEXT columns that hold a JSON map keyed by\n-- provider (LLM carrier for `model_by_provider_json`, voice provider\n-- for `voice_by_provider_json`). The reconcile pass now:\n-- 1. Phase 1 \xB7 snapshots the current `model_v` / `voice_json` into\n-- `bucket[priorProvider]` BEFORE overwriting.\n-- 2. Phase 2 \xB7 reads `bucket[newProvider]` and restores from it if\n-- present (reachability-checked for models). Falls back to the\n-- existing random-fast-pool pick when the bucket is empty / stale.\n--\n-- Effect \xB7 switching provider feels like a SIM swap. Every per-agent\n-- config that existed on a provider is preserved and restored on the\n-- next visit to that provider.\n--\n-- Purely additive \xB7 both columns start NULL on every existing row.\n-- No data migration step; buckets seed organically as the user picks\n-- models / switches providers post-deploy. The migration runner's\n-- \"already-applied\" guard at db.ts handles ALTER retries idempotently\n-- if a rebase ever reorders this past a column it adds elsewhere.\n\nALTER TABLE agents ADD COLUMN model_by_provider_json TEXT;\nALTER TABLE agents ADD COLUMN voice_by_provider_json TEXT;\n";
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
// src/storage/migrations/051_search_credentials.sql
|
|
876
|
+
var search_credentials_default;
|
|
877
|
+
var init_search_credentials = __esm({
|
|
878
|
+
"src/storage/migrations/051_search_credentials.sql"() {
|
|
879
|
+
search_credentials_default = "-- 051_search_credentials.sql\n--\n-- Multi-instance SEARCH provider credentials \xB7 final mirror of the\n-- pattern that migrations 044-046 introduced for LLM and migration 049\n-- introduced for Voice TTS. The same web-search provider (Brave or\n-- Tavily) can now appear multiple times with distinct user-supplied\n-- labels (personal + team account, for example), with exactly one\n-- \"active\" credential pointed to by `prefs.active_search_credential_id`.\n--\n-- After this migration runs, `provider_keys` is EMPTY \xB7 every\n-- credential the user holds (LLM / voice / search) lives in its own\n-- typed table with `(id, provider, label, key_blob)` columns. The\n-- legacy `provider_keys` table itself stays in the schema for now \xB7\n-- it's structurally tiny and dropping it would require coordinated\n-- removal of the `getKey` / `setKey` helpers in `keys.ts`, which is\n-- noise unrelated to this feature.\n--\n-- Unlike the LLM and voice flips, switching active search provider\n-- has NO per-agent reshuffle \xB7 agents don't carry per-search-provider\n-- state, so the active swap is purely a routing decision that the\n-- next web-search call honours automatically. No `reconcile-*`\n-- helper is needed.\n\nCREATE TABLE IF NOT EXISTS search_credentials (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL, -- 'brave' | 'tavily'\n label TEXT NOT NULL,\n key_blob BLOB NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS search_credentials_provider_idx\n ON search_credentials(provider);\n\nALTER TABLE prefs ADD COLUMN active_search_credential_id TEXT;\n\n-- One-shot migration \xB7 move every configured brave/tavily row from\n-- provider_keys into search_credentials. Labels default to the\n-- provider's display name; the seed-active step below picks brave\n-- when both exist (matches the historical `webSearchProvider` default\n-- of \"brave\" set in migration 029).\nINSERT INTO search_credentials (id, provider, label, key_blob, created_at, updated_at)\nSELECT\n lower(hex(randomblob(8))) AS id,\n provider,\n CASE provider\n WHEN 'brave' THEN 'Brave Search'\n WHEN 'tavily' THEN 'Tavily Search'\n ELSE provider\n END AS label,\n key_blob,\n created_at,\n updated_at\nFROM provider_keys\nWHERE provider IN ('brave','tavily')\n AND length(key_blob) > 0;\n\n-- Seed prefs.active_search_credential_id with the highest-priority\n-- migrated credential. Priority mirrors the historical default in\n-- migration 029 (brave first, tavily as alternate). If the user had\n-- previously expressed a `web_search_provider = 'tavily'` preference,\n-- that's preserved here by sorting on that pref first.\nUPDATE prefs\n SET active_search_credential_id = (\n SELECT id FROM search_credentials\n ORDER BY CASE provider\n WHEN (SELECT COALESCE(web_search_provider, 'brave') FROM prefs WHERE id = 1) THEN 0\n WHEN 'brave' THEN 1\n WHEN 'tavily' THEN 2\n END,\n created_at ASC\n LIMIT 1\n )\n WHERE id = 1\n AND active_search_credential_id IS NULL;\n\n-- Finally \xB7 remove the migrated search rows from provider_keys so\n-- the legacy table is fully drained (all four credentialed providers\n-- \xB7 openai/anthropic/google/xai for LLM, minimax/elevenlabs for voice,\n-- brave/tavily for search \xB7 now live in their typed tables). After\n-- this migration, `getKey(\"brave\")` and `getKey(\"tavily\")` always\n-- return null \xB7 callers must read the active search credential via\n-- `getActiveSearchKeyPlaintext()` instead.\nDELETE FROM provider_keys\n WHERE provider IN ('brave','tavily');\n";
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// src/storage/migrations/052_room_name_auto.sql
|
|
884
|
+
var room_name_auto_default;
|
|
885
|
+
var init_room_name_auto = __esm({
|
|
886
|
+
"src/storage/migrations/052_room_name_auto.sql"() {
|
|
887
|
+
room_name_auto_default = "-- 052_room_name_auto.sql\n-- Track whether a room's `name` was auto-derived from the opening\n-- question's first 60 chars (default \xB7 1) or explicitly set by the\n-- client at creation (0). The orchestrator's round-1-complete hook\n-- runs an LLM topic-phrase summariser and writes a short title back\n-- ONLY when name_auto = 1 (SQL-side guard via UPDATE WHERE), so any\n-- future rename UI can flip the flag to 0 without racing the auto\n-- pipeline.\n\nALTER TABLE rooms ADD COLUMN name_auto INTEGER NOT NULL DEFAULT 1;\n";
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
|
|
859
891
|
// src/storage/db.ts
|
|
860
892
|
var db_exports = {};
|
|
861
893
|
__export(db_exports, {
|
|
@@ -978,6 +1010,10 @@ var init_db = __esm({
|
|
|
978
1010
|
init_llm_credentials();
|
|
979
1011
|
init_drop_topic_recs();
|
|
980
1012
|
init_user_long_memory();
|
|
1013
|
+
init_voice_credentials();
|
|
1014
|
+
init_agent_provider_buckets();
|
|
1015
|
+
init_search_credentials();
|
|
1016
|
+
init_room_name_auto();
|
|
981
1017
|
MIGRATIONS = [
|
|
982
1018
|
{ name: "001_init.sql", sql: init_default },
|
|
983
1019
|
{ name: "002_default_opus.sql", sql: default_opus_default },
|
|
@@ -1026,7 +1062,11 @@ var init_db = __esm({
|
|
|
1026
1062
|
{ name: "045_active_llm_provider_pref.sql", sql: active_llm_provider_pref_default },
|
|
1027
1063
|
{ name: "046_llm_credentials.sql", sql: llm_credentials_default },
|
|
1028
1064
|
{ name: "047_drop_topic_recs.sql", sql: drop_topic_recs_default },
|
|
1029
|
-
{ name: "048_user_long_memory.sql", sql: user_long_memory_default }
|
|
1065
|
+
{ name: "048_user_long_memory.sql", sql: user_long_memory_default },
|
|
1066
|
+
{ name: "049_voice_credentials.sql", sql: voice_credentials_default },
|
|
1067
|
+
{ name: "050_agent_provider_buckets.sql", sql: agent_provider_buckets_default },
|
|
1068
|
+
{ name: "051_search_credentials.sql", sql: search_credentials_default },
|
|
1069
|
+
{ name: "052_room_name_auto.sql", sql: room_name_auto_default }
|
|
1030
1070
|
];
|
|
1031
1071
|
_db = null;
|
|
1032
1072
|
}
|
|
@@ -1177,7 +1217,9 @@ var VALID_CARRIER_PREFS = /* @__PURE__ */ new Set([
|
|
|
1177
1217
|
"anthropic",
|
|
1178
1218
|
"openai",
|
|
1179
1219
|
"google",
|
|
1180
|
-
"xai"
|
|
1220
|
+
"xai",
|
|
1221
|
+
"moonshot",
|
|
1222
|
+
"zhipu"
|
|
1181
1223
|
]);
|
|
1182
1224
|
function parseCarrierPref(raw) {
|
|
1183
1225
|
if (!raw) return null;
|
|
@@ -1400,6 +1442,54 @@ function parseBuildLog(raw) {
|
|
|
1400
1442
|
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
1401
1443
|
};
|
|
1402
1444
|
}
|
|
1445
|
+
function parseModelByProvider(raw) {
|
|
1446
|
+
if (!raw) return {};
|
|
1447
|
+
try {
|
|
1448
|
+
const obj = JSON.parse(raw);
|
|
1449
|
+
if (!obj || typeof obj !== "object") return {};
|
|
1450
|
+
const out = {};
|
|
1451
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1452
|
+
if (VALID_CARRIER_PREFS.has(k) && typeof v === "string" && v.trim().length > 0) {
|
|
1453
|
+
out[k] = v.trim();
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return out;
|
|
1457
|
+
} catch {
|
|
1458
|
+
return {};
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
function parseVoiceByProvider(raw) {
|
|
1462
|
+
if (!raw) return {};
|
|
1463
|
+
try {
|
|
1464
|
+
const obj = JSON.parse(raw);
|
|
1465
|
+
if (!obj || typeof obj !== "object") return {};
|
|
1466
|
+
const out = {};
|
|
1467
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1468
|
+
if (k !== "minimax" && k !== "elevenlabs") continue;
|
|
1469
|
+
const profile = parseVoice(JSON.stringify(v));
|
|
1470
|
+
if (profile) out[k] = profile;
|
|
1471
|
+
}
|
|
1472
|
+
return out;
|
|
1473
|
+
} catch {
|
|
1474
|
+
return {};
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function serializeModelByProvider(b) {
|
|
1478
|
+
if (!b || Object.keys(b).length === 0) return null;
|
|
1479
|
+
return JSON.stringify(b);
|
|
1480
|
+
}
|
|
1481
|
+
function serializeVoiceByProvider(b) {
|
|
1482
|
+
if (!b || Object.keys(b).length === 0) return null;
|
|
1483
|
+
const out = {};
|
|
1484
|
+
for (const [k, v] of Object.entries(b)) {
|
|
1485
|
+
if (!v) continue;
|
|
1486
|
+
const json = serializeVoice(v);
|
|
1487
|
+
if (!json) continue;
|
|
1488
|
+
out[k] = JSON.parse(json);
|
|
1489
|
+
}
|
|
1490
|
+
if (Object.keys(out).length === 0) return null;
|
|
1491
|
+
return JSON.stringify(out);
|
|
1492
|
+
}
|
|
1403
1493
|
function serializeVoice(v) {
|
|
1404
1494
|
if (!v) return null;
|
|
1405
1495
|
const provider = VALID_VOICE_PROVIDERS.has(v.provider) ? v.provider : null;
|
|
@@ -1447,7 +1537,7 @@ function mapRow(row) {
|
|
|
1447
1537
|
updatedAt: row.updated_at
|
|
1448
1538
|
};
|
|
1449
1539
|
}
|
|
1450
|
-
var SELECT_COLS = "id, name, handle, role_tag, role_kind, bio, cover_quote, instruction, model_v, carrier_pref, avatar_path, ability_json, is_pinned, is_seed, web_search_enabled, voice_json, persona_spec_json, created_at, updated_at";
|
|
1540
|
+
var SELECT_COLS = "id, name, handle, role_tag, role_kind, bio, cover_quote, instruction, model_v, carrier_pref, avatar_path, ability_json, is_pinned, is_seed, web_search_enabled, voice_json, persona_spec_json, model_by_provider_json, voice_by_provider_json, created_at, updated_at";
|
|
1451
1541
|
function listAgents() {
|
|
1452
1542
|
const rows = getDb().prepare(
|
|
1453
1543
|
`SELECT ${SELECT_COLS} FROM agents
|
|
@@ -1756,6 +1846,47 @@ function updateAgent(id, patch) {
|
|
|
1756
1846
|
if (r.changes === 0) return null;
|
|
1757
1847
|
return getAgent(id);
|
|
1758
1848
|
}
|
|
1849
|
+
function getModelBucket(id) {
|
|
1850
|
+
const row = getDb().prepare("SELECT model_by_provider_json FROM agents WHERE id = ?").get(id);
|
|
1851
|
+
if (!row) return {};
|
|
1852
|
+
return parseModelByProvider(row.model_by_provider_json);
|
|
1853
|
+
}
|
|
1854
|
+
function getVoiceBucket(id) {
|
|
1855
|
+
const row = getDb().prepare("SELECT voice_by_provider_json FROM agents WHERE id = ?").get(id);
|
|
1856
|
+
if (!row) return {};
|
|
1857
|
+
return parseVoiceByProvider(row.voice_by_provider_json);
|
|
1858
|
+
}
|
|
1859
|
+
function writeModelBucketEntry(id, carrier, modelV) {
|
|
1860
|
+
if (!modelV) return;
|
|
1861
|
+
const bucket = getModelBucket(id);
|
|
1862
|
+
if (bucket[carrier] === modelV) return;
|
|
1863
|
+
bucket[carrier] = modelV;
|
|
1864
|
+
const serialized = serializeModelByProvider(bucket);
|
|
1865
|
+
getDb().prepare("UPDATE agents SET model_by_provider_json = ?, updated_at = ? WHERE id = ?").run(serialized, Date.now(), id);
|
|
1866
|
+
}
|
|
1867
|
+
function writeVoiceBucketEntry(id, provider, voice) {
|
|
1868
|
+
const normalized = parseVoice(serializeVoice(voice));
|
|
1869
|
+
if (!normalized || normalized.provider !== "minimax" && normalized.provider !== "elevenlabs") return;
|
|
1870
|
+
if (normalized.provider !== provider) return;
|
|
1871
|
+
const bucket = getVoiceBucket(id);
|
|
1872
|
+
bucket[provider] = normalized;
|
|
1873
|
+
const serialized = serializeVoiceByProvider(bucket);
|
|
1874
|
+
getDb().prepare("UPDATE agents SET voice_by_provider_json = ?, updated_at = ? WHERE id = ?").run(serialized, Date.now(), id);
|
|
1875
|
+
}
|
|
1876
|
+
function deleteModelBucketEntry(id, carrier) {
|
|
1877
|
+
const bucket = getModelBucket(id);
|
|
1878
|
+
if (!(carrier in bucket)) return;
|
|
1879
|
+
delete bucket[carrier];
|
|
1880
|
+
const serialized = serializeModelByProvider(bucket);
|
|
1881
|
+
getDb().prepare("UPDATE agents SET model_by_provider_json = ?, updated_at = ? WHERE id = ?").run(serialized, Date.now(), id);
|
|
1882
|
+
}
|
|
1883
|
+
function deleteVoiceBucketEntry(id, provider) {
|
|
1884
|
+
const bucket = getVoiceBucket(id);
|
|
1885
|
+
if (!(provider in bucket)) return;
|
|
1886
|
+
delete bucket[provider];
|
|
1887
|
+
const serialized = serializeVoiceByProvider(bucket);
|
|
1888
|
+
getDb().prepare("UPDATE agents SET voice_by_provider_json = ?, updated_at = ? WHERE id = ?").run(serialized, Date.now(), id);
|
|
1889
|
+
}
|
|
1759
1890
|
|
|
1760
1891
|
// src/seed/run.ts
|
|
1761
1892
|
init_db();
|
|
@@ -2474,7 +2605,7 @@ function runSeed() {
|
|
|
2474
2605
|
// src/server.ts
|
|
2475
2606
|
import { serve } from "@hono/node-server";
|
|
2476
2607
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
2477
|
-
import { Hono as
|
|
2608
|
+
import { Hono as Hono15 } from "hono";
|
|
2478
2609
|
import { existsSync as existsSync2 } from "fs";
|
|
2479
2610
|
|
|
2480
2611
|
// src/routes/agents.ts
|
|
@@ -2649,10 +2780,11 @@ var MODELS = {
|
|
|
2649
2780
|
deck: "V4 Flash \xB7 fast \xB7 1M ctx",
|
|
2650
2781
|
viaUniversalOnly: true
|
|
2651
2782
|
},
|
|
2652
|
-
// ── Zhipu (Z.AI) · GLM family · OR + B.AI
|
|
2653
|
-
//
|
|
2654
|
-
//
|
|
2655
|
-
//
|
|
2783
|
+
// ── Zhipu (Z.AI) · GLM family · direct + OR + B.AI ──
|
|
2784
|
+
// Direct route uses Zhipu's OpenAI-compatible chat-completions API
|
|
2785
|
+
// at https://open.bigmodel.cn/api/paas/v4/ (see adapter.ts
|
|
2786
|
+
// case "zhipu"). OpenRouter catalog convention: `z-ai/glm-X.Y`.
|
|
2787
|
+
// B.AI uses dotted lowercase: `glm-5.1`.
|
|
2656
2788
|
"glm-5-1": {
|
|
2657
2789
|
v: "glm-5-1",
|
|
2658
2790
|
provider: "zhipu",
|
|
@@ -2661,16 +2793,16 @@ var MODELS = {
|
|
|
2661
2793
|
baiId: "glm-5.1",
|
|
2662
2794
|
displayName: "GLM 5.1",
|
|
2663
2795
|
contextBudget: 2e5,
|
|
2664
|
-
deck: "Zhipu flagship \xB7 200k ctx"
|
|
2665
|
-
viaUniversalOnly: true
|
|
2796
|
+
deck: "Zhipu flagship \xB7 200k ctx"
|
|
2666
2797
|
},
|
|
2667
|
-
// ── Moonshot · Kimi family · OR + B.AI ──
|
|
2798
|
+
// ── Moonshot · Kimi family · direct + OR + B.AI ──
|
|
2799
|
+
// Direct route uses Moonshot's OpenAI-compatible chat-completions
|
|
2800
|
+
// API at https://api.moonshot.cn/v1 (see adapter.ts case "moonshot").
|
|
2668
2801
|
// OpenRouter catalog convention: `moonshotai/kimi-k2.6` (the leading
|
|
2669
2802
|
// `k` is part of the slug — `moonshotai/kimi-2.6` 404s). B.AI's
|
|
2670
2803
|
// siliconflow distributor still ships the older `kimi-k2.5` channel
|
|
2671
2804
|
// (per 2026-05-17 catalog snapshot), so the B.AI route serves K2.5
|
|
2672
|
-
// until B.AI picks up the newer build.
|
|
2673
|
-
// viaUniversalOnly skips the direct path.
|
|
2805
|
+
// until B.AI picks up the newer build.
|
|
2674
2806
|
"kimi-k2-6": {
|
|
2675
2807
|
v: "kimi-k2-6",
|
|
2676
2808
|
provider: "moonshot",
|
|
@@ -2679,8 +2811,7 @@ var MODELS = {
|
|
|
2679
2811
|
baiId: "kimi-k2.5",
|
|
2680
2812
|
displayName: "Kimi K2.6",
|
|
2681
2813
|
contextBudget: 256e3,
|
|
2682
|
-
deck: "Moonshot \xB7 long-context"
|
|
2683
|
-
viaUniversalOnly: true
|
|
2814
|
+
deck: "Moonshot \xB7 long-context"
|
|
2684
2815
|
},
|
|
2685
2816
|
// ── MiniMax · M-series · OR + B.AI ──
|
|
2686
2817
|
// No direct @ai-sdk client · viaUniversalOnly skips the direct path.
|
|
@@ -2777,7 +2908,11 @@ var PROFILE_SYSTEM = [
|
|
|
2777
2908
|
"Constraints:",
|
|
2778
2909
|
"\xB7 DO NOT use generic personality words. Every entry names a person / case / concept / position.",
|
|
2779
2910
|
"\xB7 If the user description maps to a real domain (VC, product, security, biotech, monetary policy, etc.), prefer NAMED references from that domain.",
|
|
2780
|
-
"\xB7 Avoid recreating the canonical six (Socrates, First Principles, Long Horizon, etc.) \u2014 pick a distinct angle."
|
|
2911
|
+
"\xB7 Avoid recreating the canonical six (Socrates, First Principles, Long Horizon, etc.) \u2014 pick a distinct angle.",
|
|
2912
|
+
"",
|
|
2913
|
+
"## CRITICAL \xB7 output format",
|
|
2914
|
+
"",
|
|
2915
|
+
"Your ENTIRE response is the JSON object inside ONE ```json fenced block. NO prose before. NO prose after. NO chain-of-thought, NO 'Here is...', NO 'I'll create...'. Use the EXACT field names from the schema above (`intellectualLineage`, `loadBearingConcepts`, `referentSet`, `failureModes`, `contrarianTakes` \u2014 camelCase, not snake_case, not abbreviated). If you cannot produce strong content for a field, emit an empty array `[]` for that field \u2014 DO NOT omit it, DO NOT substitute a different field name, DO NOT explain in prose why it's empty."
|
|
2781
2916
|
].join("\n");
|
|
2782
2917
|
var HOUSE_STYLE = [
|
|
2783
2918
|
"## Boardroom directors \xB7 house style",
|
|
@@ -2898,7 +3033,7 @@ function buildAgentProfileMessages(opts) {
|
|
|
2898
3033
|
}
|
|
2899
3034
|
userBody.push(
|
|
2900
3035
|
"",
|
|
2901
|
-
"Now produce the profile JSON object as specified."
|
|
3036
|
+
"Now produce the profile JSON object as specified \u2014 ```json fenced block, exact camelCase field names, no prose before or after."
|
|
2902
3037
|
);
|
|
2903
3038
|
return [
|
|
2904
3039
|
{ role: "system", content: PROFILE_SYSTEM },
|
|
@@ -3039,29 +3174,98 @@ function parseAgentSpec(raw) {
|
|
|
3039
3174
|
ability
|
|
3040
3175
|
};
|
|
3041
3176
|
}
|
|
3177
|
+
function pickArrayField(parsed, aliases) {
|
|
3178
|
+
for (const k of aliases) {
|
|
3179
|
+
const v = parsed[k];
|
|
3180
|
+
if (Array.isArray(v)) return v;
|
|
3181
|
+
}
|
|
3182
|
+
return [];
|
|
3183
|
+
}
|
|
3184
|
+
function pickObjectField(parsed, aliases) {
|
|
3185
|
+
for (const k of aliases) {
|
|
3186
|
+
const v = parsed[k];
|
|
3187
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
3188
|
+
}
|
|
3189
|
+
return {};
|
|
3190
|
+
}
|
|
3191
|
+
function coerceConceptEntry(c) {
|
|
3192
|
+
if (typeof c === "string") {
|
|
3193
|
+
const s = c.trim();
|
|
3194
|
+
const m = /^(.+?)\s*[·:\-—]\s*(.+)$/.exec(s);
|
|
3195
|
+
if (m) return { name: clamp(m[1].trim(), 80), gloss: clamp(m[2].trim(), 200) };
|
|
3196
|
+
return { name: clamp(s, 80), gloss: "" };
|
|
3197
|
+
}
|
|
3198
|
+
if (!c || typeof c !== "object") return { name: "", gloss: "" };
|
|
3199
|
+
const o = c;
|
|
3200
|
+
const name = [o.name, o.concept, o.title, o.handle, o.term].find(
|
|
3201
|
+
(x) => typeof x === "string" && x.trim().length > 0
|
|
3202
|
+
);
|
|
3203
|
+
const gloss = [o.gloss, o.description, o.desc, o.detail, o.explanation, o.summary].find(
|
|
3204
|
+
(x) => typeof x === "string" && x.trim().length > 0
|
|
3205
|
+
);
|
|
3206
|
+
return {
|
|
3207
|
+
name: name ? clamp(name.trim(), 80) : "",
|
|
3208
|
+
gloss: gloss ? clamp(gloss.trim(), 200) : ""
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
function coerceReferentEntry(r) {
|
|
3212
|
+
if (typeof r === "string") {
|
|
3213
|
+
const s = r.trim();
|
|
3214
|
+
const m = /^(.+?)\s*[·:\-—]\s*(.+)$/.exec(s);
|
|
3215
|
+
if (m) return { ref: clamp(m[1].trim(), 80), why: clamp(m[2].trim(), 200) };
|
|
3216
|
+
return { ref: clamp(s, 80), why: "" };
|
|
3217
|
+
}
|
|
3218
|
+
if (!r || typeof r !== "object") return { ref: "", why: "" };
|
|
3219
|
+
const o = r;
|
|
3220
|
+
const ref = [o.ref, o.name, o.reference, o.anchor, o.title, o.case].find(
|
|
3221
|
+
(x) => typeof x === "string" && x.trim().length > 0
|
|
3222
|
+
);
|
|
3223
|
+
const why = [o.why, o.reason, o.relevance, o.gloss, o.description, o.note].find(
|
|
3224
|
+
(x) => typeof x === "string" && x.trim().length > 0
|
|
3225
|
+
);
|
|
3226
|
+
return {
|
|
3227
|
+
ref: ref ? clamp(ref.trim(), 80) : "",
|
|
3228
|
+
why: why ? clamp(why.trim(), 200) : ""
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3042
3231
|
function parseAgentProfile(raw) {
|
|
3043
3232
|
const parsed = extractJson(raw);
|
|
3044
3233
|
if (!parsed) return null;
|
|
3045
|
-
const lineage = parsed
|
|
3046
|
-
const influencedByRaw =
|
|
3047
|
-
const opposedToRaw =
|
|
3234
|
+
const lineage = pickObjectField(parsed, ["intellectualLineage", "intellectual_lineage", "lineage"]);
|
|
3235
|
+
const influencedByRaw = pickArrayField(lineage, ["influencedBy", "influenced_by", "influences", "influenced"]);
|
|
3236
|
+
const opposedToRaw = pickArrayField(lineage, ["opposedTo", "opposed_to", "opposes", "against"]);
|
|
3048
3237
|
const influencedBy = influencedByRaw.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => clamp(s.trim(), 200)).slice(0, 5);
|
|
3049
3238
|
const opposedTo = opposedToRaw.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => clamp(s.trim(), 200)).slice(0, 4);
|
|
3050
|
-
const conceptsRaw =
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3239
|
+
const conceptsRaw = pickArrayField(parsed, [
|
|
3240
|
+
"loadBearingConcepts",
|
|
3241
|
+
"load_bearing_concepts",
|
|
3242
|
+
"concepts",
|
|
3243
|
+
"frames",
|
|
3244
|
+
"mentalTools",
|
|
3245
|
+
"mental_tools"
|
|
3246
|
+
]);
|
|
3247
|
+
const loadBearingConcepts = conceptsRaw.map(coerceConceptEntry).filter((c) => c.name.length > 0).slice(0, 6);
|
|
3248
|
+
const referentsRaw = pickArrayField(parsed, [
|
|
3249
|
+
"referentSet",
|
|
3250
|
+
"referent_set",
|
|
3251
|
+
"referents",
|
|
3252
|
+
"anchors",
|
|
3253
|
+
"references",
|
|
3254
|
+
"citations"
|
|
3255
|
+
]);
|
|
3256
|
+
const referentSet = referentsRaw.map(coerceReferentEntry).filter((r) => r.ref.length > 0).slice(0, 6);
|
|
3257
|
+
const failureModesRaw = pickArrayField(parsed, ["failureModes", "failure_modes", "blindSpots", "blind_spots"]);
|
|
3061
3258
|
const failureModes = failureModesRaw.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => clamp(s.trim(), 220)).slice(0, 4);
|
|
3062
|
-
const contrarianTakesRaw =
|
|
3259
|
+
const contrarianTakesRaw = pickArrayField(parsed, [
|
|
3260
|
+
"contrarianTakes",
|
|
3261
|
+
"contrarian_takes",
|
|
3262
|
+
"contrarianViews",
|
|
3263
|
+
"contrarian_views",
|
|
3264
|
+
"takes"
|
|
3265
|
+
]);
|
|
3063
3266
|
const contrarianTakes = contrarianTakesRaw.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => clamp(s.trim(), 220)).slice(0, 4);
|
|
3064
|
-
|
|
3267
|
+
const anyPopulated = loadBearingConcepts.length > 0 || referentSet.length > 0 || influencedBy.length > 0 || opposedTo.length > 0 || failureModes.length > 0 || contrarianTakes.length > 0;
|
|
3268
|
+
if (!anyPopulated) return null;
|
|
3065
3269
|
return {
|
|
3066
3270
|
intellectualLineage: { influencedBy, opposedTo },
|
|
3067
3271
|
loadBearingConcepts,
|
|
@@ -3551,8 +3755,7 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
|
3551
3755
|
import { APICallError, streamText } from "ai";
|
|
3552
3756
|
|
|
3553
3757
|
// src/storage/credentials.ts
|
|
3554
|
-
import {
|
|
3555
|
-
import { userInfo } from "os";
|
|
3758
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
3556
3759
|
|
|
3557
3760
|
// src/ai/providers.ts
|
|
3558
3761
|
var MULTI_MODEL_LLM_PROVIDERS = [
|
|
@@ -3563,7 +3766,9 @@ var SINGLE_MODEL_LLM_PROVIDERS = [
|
|
|
3563
3766
|
"anthropic",
|
|
3564
3767
|
"openai",
|
|
3565
3768
|
"google",
|
|
3566
|
-
"xai"
|
|
3769
|
+
"xai",
|
|
3770
|
+
"moonshot",
|
|
3771
|
+
"zhipu"
|
|
3567
3772
|
];
|
|
3568
3773
|
var ALL_LLM_PROVIDERS = [
|
|
3569
3774
|
...MULTI_MODEL_LLM_PROVIDERS,
|
|
@@ -3575,7 +3780,9 @@ var LLM_PROVIDER_PRIORITY = [
|
|
|
3575
3780
|
"anthropic",
|
|
3576
3781
|
"openai",
|
|
3577
3782
|
"google",
|
|
3578
|
-
"xai"
|
|
3783
|
+
"xai",
|
|
3784
|
+
"moonshot",
|
|
3785
|
+
"zhipu"
|
|
3579
3786
|
];
|
|
3580
3787
|
function isMultiModelProvider(p) {
|
|
3581
3788
|
return p === "openrouter" || p === "bai";
|
|
@@ -3586,6 +3793,10 @@ function isLlmProvider(p) {
|
|
|
3586
3793
|
|
|
3587
3794
|
// src/storage/credentials.ts
|
|
3588
3795
|
init_db();
|
|
3796
|
+
|
|
3797
|
+
// src/storage/credential-crypto.ts
|
|
3798
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
|
|
3799
|
+
import { userInfo } from "os";
|
|
3589
3800
|
var SALT = "boardroom.v1.salt";
|
|
3590
3801
|
var ALGO = "aes-256-gcm";
|
|
3591
3802
|
var _key = null;
|
|
@@ -3595,14 +3806,14 @@ function deriveKey() {
|
|
|
3595
3806
|
_key = scryptSync(username, SALT, 32);
|
|
3596
3807
|
return _key;
|
|
3597
3808
|
}
|
|
3598
|
-
function
|
|
3809
|
+
function encryptCredential(plain) {
|
|
3599
3810
|
const iv = randomBytes(12);
|
|
3600
3811
|
const cipher = createCipheriv(ALGO, deriveKey(), iv);
|
|
3601
3812
|
const ct = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
3602
3813
|
const tag = cipher.getAuthTag();
|
|
3603
3814
|
return Buffer.concat([iv, tag, ct]);
|
|
3604
3815
|
}
|
|
3605
|
-
function
|
|
3816
|
+
function decryptCredential(blob) {
|
|
3606
3817
|
const iv = blob.subarray(0, 12);
|
|
3607
3818
|
const tag = blob.subarray(12, 28);
|
|
3608
3819
|
const ct = blob.subarray(28);
|
|
@@ -3610,7 +3821,7 @@ function decrypt(blob) {
|
|
|
3610
3821
|
decipher.setAuthTag(tag);
|
|
3611
3822
|
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
|
|
3612
3823
|
}
|
|
3613
|
-
function
|
|
3824
|
+
function maskCredential(plain) {
|
|
3614
3825
|
const trimmed = plain.trim();
|
|
3615
3826
|
if (!trimmed) return "";
|
|
3616
3827
|
const n = trimmed.length;
|
|
@@ -3618,11 +3829,13 @@ function maskKey(plain) {
|
|
|
3618
3829
|
if (n <= 12) return `${trimmed.slice(0, 2)}${"\u2022".repeat(n - 4)}${trimmed.slice(-2)}`;
|
|
3619
3830
|
return `${trimmed.slice(0, 4)}${"\u2022".repeat(n - 8)}${trimmed.slice(-4)}`;
|
|
3620
3831
|
}
|
|
3832
|
+
|
|
3833
|
+
// src/storage/credentials.ts
|
|
3621
3834
|
function rowToMeta(row) {
|
|
3622
3835
|
if (!isLlmProvider(row.provider)) return null;
|
|
3623
3836
|
let preview = "";
|
|
3624
3837
|
try {
|
|
3625
|
-
preview =
|
|
3838
|
+
preview = maskCredential(decryptCredential(row.key_blob));
|
|
3626
3839
|
} catch {
|
|
3627
3840
|
preview = "";
|
|
3628
3841
|
}
|
|
@@ -3648,7 +3861,7 @@ function getLlmCredentialKey(id) {
|
|
|
3648
3861
|
const row = getDb().prepare("SELECT key_blob FROM llm_credentials WHERE id = ?").get(id);
|
|
3649
3862
|
if (!row) return null;
|
|
3650
3863
|
try {
|
|
3651
|
-
return
|
|
3864
|
+
return decryptCredential(row.key_blob);
|
|
3652
3865
|
} catch {
|
|
3653
3866
|
return null;
|
|
3654
3867
|
}
|
|
@@ -3679,6 +3892,15 @@ function providerDisplayName(provider) {
|
|
|
3679
3892
|
return "Gemini";
|
|
3680
3893
|
case "xai":
|
|
3681
3894
|
return "Grok";
|
|
3895
|
+
// Brand-name labels for the two newest direct providers · "Kimi"
|
|
3896
|
+
// (model brand) over "Moonshot" (company), "GLM" (model family)
|
|
3897
|
+
// over "Zhipu" (company) · same convention as "Claude"/anthropic
|
|
3898
|
+
// and "ChatGPT"/openai · the user-facing UI prefers the product
|
|
3899
|
+
// name over the corporate slug.
|
|
3900
|
+
case "moonshot":
|
|
3901
|
+
return "Kimi";
|
|
3902
|
+
case "zhipu":
|
|
3903
|
+
return "GLM";
|
|
3682
3904
|
}
|
|
3683
3905
|
}
|
|
3684
3906
|
function createLlmCredential(provider, label, plain) {
|
|
@@ -3686,8 +3908,8 @@ function createLlmCredential(provider, label, plain) {
|
|
|
3686
3908
|
if (!trimmed) return null;
|
|
3687
3909
|
if (!ALL_LLM_PROVIDERS.includes(provider)) return null;
|
|
3688
3910
|
const resolvedLabel = resolveFreeLabel(provider, label);
|
|
3689
|
-
const id =
|
|
3690
|
-
const blob =
|
|
3911
|
+
const id = randomBytes2(8).toString("hex");
|
|
3912
|
+
const blob = encryptCredential(trimmed);
|
|
3691
3913
|
const now = Date.now();
|
|
3692
3914
|
getDb().prepare(
|
|
3693
3915
|
`INSERT INTO llm_credentials (id, provider, label, key_blob, created_at, updated_at)
|
|
@@ -3721,6 +3943,8 @@ function mapRow3(row) {
|
|
|
3721
3943
|
minimaxRegion: normalizeMinimaxRegion(row.minimax_region),
|
|
3722
3944
|
activeLlmProvider: raw && isLlmProvider(raw) ? raw : null,
|
|
3723
3945
|
activeLlmCredentialId: row.active_llm_credential_id,
|
|
3946
|
+
activeVoiceCredentialId: row.active_voice_credential_id,
|
|
3947
|
+
activeSearchCredentialId: row.active_search_credential_id,
|
|
3724
3948
|
createdAt: row.created_at,
|
|
3725
3949
|
updatedAt: row.updated_at
|
|
3726
3950
|
};
|
|
@@ -3732,6 +3956,8 @@ function getPrefs() {
|
|
|
3732
3956
|
COALESCE(minimax_region, 'cn') AS minimax_region,
|
|
3733
3957
|
active_llm_provider,
|
|
3734
3958
|
active_llm_credential_id,
|
|
3959
|
+
active_voice_credential_id,
|
|
3960
|
+
active_search_credential_id,
|
|
3735
3961
|
created_at, updated_at FROM prefs WHERE id = 1`
|
|
3736
3962
|
).get();
|
|
3737
3963
|
if (!row) {
|
|
@@ -3774,6 +4000,14 @@ function updatePrefs(patch) {
|
|
|
3774
4000
|
fields.push("active_llm_credential_id = ?");
|
|
3775
4001
|
values.push(patch.activeLlmCredentialId);
|
|
3776
4002
|
}
|
|
4003
|
+
if (patch.activeVoiceCredentialId !== void 0) {
|
|
4004
|
+
fields.push("active_voice_credential_id = ?");
|
|
4005
|
+
values.push(patch.activeVoiceCredentialId);
|
|
4006
|
+
}
|
|
4007
|
+
if (patch.activeSearchCredentialId !== void 0) {
|
|
4008
|
+
fields.push("active_search_credential_id = ?");
|
|
4009
|
+
values.push(patch.activeSearchCredentialId);
|
|
4010
|
+
}
|
|
3777
4011
|
if (fields.length === 0) return getPrefs();
|
|
3778
4012
|
fields.push("updated_at = ?");
|
|
3779
4013
|
values.push(Date.now());
|
|
@@ -3983,6 +4217,30 @@ function directResolved(meta, apiKey) {
|
|
|
3983
4217
|
carrier: "xai"
|
|
3984
4218
|
};
|
|
3985
4219
|
}
|
|
4220
|
+
case "moonshot": {
|
|
4221
|
+
const compat = createOpenAICompatible({
|
|
4222
|
+
name: "moonshot",
|
|
4223
|
+
apiKey,
|
|
4224
|
+
baseURL: "https://api.moonshot.cn/v1",
|
|
4225
|
+
fetch: makeLoggedFetch("moonshot")
|
|
4226
|
+
});
|
|
4227
|
+
return {
|
|
4228
|
+
model: compat.chatModel(meta.directApiId),
|
|
4229
|
+
carrier: "moonshot"
|
|
4230
|
+
};
|
|
4231
|
+
}
|
|
4232
|
+
case "zhipu": {
|
|
4233
|
+
const compat = createOpenAICompatible({
|
|
4234
|
+
name: "zhipu",
|
|
4235
|
+
apiKey,
|
|
4236
|
+
baseURL: "https://open.bigmodel.cn/api/paas/v4/",
|
|
4237
|
+
fetch: makeLoggedFetch("zhipu")
|
|
4238
|
+
});
|
|
4239
|
+
return {
|
|
4240
|
+
model: compat.chatModel(meta.directApiId),
|
|
4241
|
+
carrier: "zhipu"
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
3986
4244
|
default:
|
|
3987
4245
|
throw new NoKeyError(meta.provider);
|
|
3988
4246
|
}
|
|
@@ -4185,6 +4443,7 @@ async function callLLM(req) {
|
|
|
4185
4443
|
async function callLLMWithUsage(req) {
|
|
4186
4444
|
let buf = "";
|
|
4187
4445
|
let usage = null;
|
|
4446
|
+
let finishReason = null;
|
|
4188
4447
|
for await (const chunk of callLLMStream(req)) {
|
|
4189
4448
|
if (chunk.type === "text") buf += chunk.delta;
|
|
4190
4449
|
else if (chunk.type === "error") throw new Error(chunk.message);
|
|
@@ -4194,15 +4453,118 @@ async function callLLMWithUsage(req) {
|
|
|
4194
4453
|
completionTokens: chunk.completionTokens,
|
|
4195
4454
|
totalTokens: chunk.totalTokens
|
|
4196
4455
|
};
|
|
4456
|
+
} else if (chunk.type === "done") {
|
|
4457
|
+
if (typeof chunk.finishReason === "string" && chunk.finishReason.length > 0) {
|
|
4458
|
+
finishReason = chunk.finishReason;
|
|
4459
|
+
}
|
|
4197
4460
|
}
|
|
4198
4461
|
}
|
|
4199
|
-
return { text: buf, usage };
|
|
4462
|
+
return { text: buf, usage, finishReason };
|
|
4200
4463
|
}
|
|
4201
4464
|
|
|
4202
4465
|
// src/storage/keys.ts
|
|
4203
4466
|
init_db();
|
|
4204
|
-
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as
|
|
4467
|
+
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes4, scryptSync as scryptSync2 } from "crypto";
|
|
4205
4468
|
import { userInfo as userInfo2 } from "os";
|
|
4469
|
+
|
|
4470
|
+
// src/storage/search-credentials.ts
|
|
4471
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
4472
|
+
init_db();
|
|
4473
|
+
var ALL_SEARCH_PROVIDERS = ["brave", "tavily"];
|
|
4474
|
+
function isSearchProvider(p) {
|
|
4475
|
+
return ALL_SEARCH_PROVIDERS.includes(p);
|
|
4476
|
+
}
|
|
4477
|
+
var SEARCH_PROVIDER_PRIORITY = ["brave", "tavily"];
|
|
4478
|
+
function rowToMeta2(row) {
|
|
4479
|
+
if (!isSearchProvider(row.provider)) return null;
|
|
4480
|
+
let preview = "";
|
|
4481
|
+
try {
|
|
4482
|
+
preview = maskCredential(decryptCredential(row.key_blob));
|
|
4483
|
+
} catch {
|
|
4484
|
+
preview = "";
|
|
4485
|
+
}
|
|
4486
|
+
return {
|
|
4487
|
+
id: row.id,
|
|
4488
|
+
provider: row.provider,
|
|
4489
|
+
label: row.label,
|
|
4490
|
+
preview,
|
|
4491
|
+
createdAt: row.created_at,
|
|
4492
|
+
updatedAt: row.updated_at
|
|
4493
|
+
};
|
|
4494
|
+
}
|
|
4495
|
+
function listSearchCredentials() {
|
|
4496
|
+
const rows = getDb().prepare("SELECT id, provider, label, key_blob, created_at, updated_at FROM search_credentials ORDER BY created_at ASC").all();
|
|
4497
|
+
return rows.map(rowToMeta2).filter((m) => m !== null);
|
|
4498
|
+
}
|
|
4499
|
+
function getSearchCredentialMeta(id) {
|
|
4500
|
+
const row = getDb().prepare("SELECT id, provider, label, key_blob, created_at, updated_at FROM search_credentials WHERE id = ?").get(id);
|
|
4501
|
+
if (!row) return null;
|
|
4502
|
+
return rowToMeta2(row);
|
|
4503
|
+
}
|
|
4504
|
+
function getSearchCredentialKey(id) {
|
|
4505
|
+
const row = getDb().prepare("SELECT key_blob FROM search_credentials WHERE id = ?").get(id);
|
|
4506
|
+
if (!row) return null;
|
|
4507
|
+
try {
|
|
4508
|
+
return decryptCredential(row.key_blob);
|
|
4509
|
+
} catch {
|
|
4510
|
+
return null;
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
function providerDisplayName2(provider) {
|
|
4514
|
+
switch (provider) {
|
|
4515
|
+
case "brave":
|
|
4516
|
+
return "Brave Search";
|
|
4517
|
+
case "tavily":
|
|
4518
|
+
return "Tavily Search";
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
function resolveFreeLabel2(provider, suggested) {
|
|
4522
|
+
const base = suggested && suggested.trim() || providerDisplayName2(provider);
|
|
4523
|
+
const existing = new Set(
|
|
4524
|
+
getDb().prepare("SELECT label FROM search_credentials").all().map((r) => r.label)
|
|
4525
|
+
);
|
|
4526
|
+
if (!existing.has(base)) return base;
|
|
4527
|
+
for (let n = 2; n < 1e3; n++) {
|
|
4528
|
+
const candidate = `${base} ${n}`;
|
|
4529
|
+
if (!existing.has(candidate)) return candidate;
|
|
4530
|
+
}
|
|
4531
|
+
return `${base} ${Date.now()}`;
|
|
4532
|
+
}
|
|
4533
|
+
function createSearchCredential(provider, label, plain) {
|
|
4534
|
+
const trimmed = plain.trim();
|
|
4535
|
+
if (!trimmed) return null;
|
|
4536
|
+
if (!isSearchProvider(provider)) return null;
|
|
4537
|
+
const resolvedLabel = resolveFreeLabel2(provider, label);
|
|
4538
|
+
const id = randomBytes3(8).toString("hex");
|
|
4539
|
+
const blob = encryptCredential(trimmed);
|
|
4540
|
+
const now = Date.now();
|
|
4541
|
+
getDb().prepare(
|
|
4542
|
+
`INSERT INTO search_credentials (id, provider, label, key_blob, created_at, updated_at)
|
|
4543
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
4544
|
+
).run(id, provider, resolvedLabel, blob, now, now);
|
|
4545
|
+
return getSearchCredentialMeta(id);
|
|
4546
|
+
}
|
|
4547
|
+
function deleteSearchCredential(id) {
|
|
4548
|
+
const meta = getSearchCredentialMeta(id);
|
|
4549
|
+
if (!meta) return null;
|
|
4550
|
+
getDb().prepare("DELETE FROM search_credentials WHERE id = ?").run(id);
|
|
4551
|
+
return meta.provider;
|
|
4552
|
+
}
|
|
4553
|
+
function resolveActiveSearchCredential() {
|
|
4554
|
+
const prefs = getPrefs();
|
|
4555
|
+
if (!prefs.activeSearchCredentialId) return null;
|
|
4556
|
+
return getSearchCredentialMeta(prefs.activeSearchCredentialId);
|
|
4557
|
+
}
|
|
4558
|
+
function getActiveSearchProvider() {
|
|
4559
|
+
return resolveActiveSearchCredential()?.provider ?? null;
|
|
4560
|
+
}
|
|
4561
|
+
function getActiveSearchKeyPlaintext() {
|
|
4562
|
+
const active = resolveActiveSearchCredential();
|
|
4563
|
+
if (!active) return null;
|
|
4564
|
+
return getSearchCredentialKey(active.id);
|
|
4565
|
+
}
|
|
4566
|
+
|
|
4567
|
+
// src/storage/keys.ts
|
|
4206
4568
|
var SALT2 = "boardroom.v1.salt";
|
|
4207
4569
|
var ALGO2 = "aes-256-gcm";
|
|
4208
4570
|
var _key2 = null;
|
|
@@ -4212,14 +4574,14 @@ function deriveKey2() {
|
|
|
4212
4574
|
_key2 = scryptSync2(username, SALT2, 32);
|
|
4213
4575
|
return _key2;
|
|
4214
4576
|
}
|
|
4215
|
-
function
|
|
4216
|
-
const iv =
|
|
4577
|
+
function encrypt(plain) {
|
|
4578
|
+
const iv = randomBytes4(12);
|
|
4217
4579
|
const cipher = createCipheriv2(ALGO2, deriveKey2(), iv);
|
|
4218
4580
|
const ct = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
4219
4581
|
const tag = cipher.getAuthTag();
|
|
4220
4582
|
return Buffer.concat([iv, tag, ct]);
|
|
4221
4583
|
}
|
|
4222
|
-
function
|
|
4584
|
+
function decrypt(blob) {
|
|
4223
4585
|
const iv = blob.subarray(0, 12);
|
|
4224
4586
|
const tag = blob.subarray(12, 28);
|
|
4225
4587
|
const ct = blob.subarray(28);
|
|
@@ -4227,36 +4589,16 @@ function decrypt2(blob) {
|
|
|
4227
4589
|
decipher.setAuthTag(tag);
|
|
4228
4590
|
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
|
|
4229
4591
|
}
|
|
4230
|
-
function hasBraveKey() {
|
|
4231
|
-
const k = getKey("brave");
|
|
4232
|
-
return typeof k === "string" && k.length > 0;
|
|
4233
|
-
}
|
|
4234
|
-
function hasTavilyKey() {
|
|
4235
|
-
const k = getKey("tavily");
|
|
4236
|
-
return typeof k === "string" && k.length > 0;
|
|
4237
|
-
}
|
|
4238
4592
|
function hasWebSearchKey() {
|
|
4239
|
-
return
|
|
4240
|
-
}
|
|
4241
|
-
function resolveWebSearchBackend(preference) {
|
|
4242
|
-
const b = hasBraveKey();
|
|
4243
|
-
const t = hasTavilyKey();
|
|
4244
|
-
if (!b && !t) return null;
|
|
4245
|
-
if (b && !t) return "brave";
|
|
4246
|
-
if (!b && t) return "tavily";
|
|
4247
|
-
if (preference === "tavily" && t) return "tavily";
|
|
4248
|
-
if (preference === "brave" && b) return "brave";
|
|
4249
|
-
return b ? "brave" : "tavily";
|
|
4593
|
+
return getActiveSearchProvider() !== null;
|
|
4250
4594
|
}
|
|
4251
4595
|
function getActiveWebSearchCredentials() {
|
|
4252
|
-
const
|
|
4253
|
-
const preference = prefRaw === "tavily" ? "tavily" : "brave";
|
|
4254
|
-
const backend = resolveWebSearchBackend(preference);
|
|
4596
|
+
const backend = getActiveSearchProvider();
|
|
4255
4597
|
if (!backend) return null;
|
|
4256
|
-
const apiKey =
|
|
4598
|
+
const apiKey = getActiveSearchKeyPlaintext();
|
|
4257
4599
|
return apiKey ? { backend, apiKey } : null;
|
|
4258
4600
|
}
|
|
4259
|
-
function
|
|
4601
|
+
function maskKey(plain) {
|
|
4260
4602
|
const trimmed = plain.trim();
|
|
4261
4603
|
if (!trimmed) return "";
|
|
4262
4604
|
const n = trimmed.length;
|
|
@@ -4273,7 +4615,7 @@ function listKeyMeta() {
|
|
|
4273
4615
|
let preview = null;
|
|
4274
4616
|
if (r.key_blob.length > 0) {
|
|
4275
4617
|
try {
|
|
4276
|
-
preview =
|
|
4618
|
+
preview = maskKey(decrypt(r.key_blob));
|
|
4277
4619
|
} catch {
|
|
4278
4620
|
preview = null;
|
|
4279
4621
|
}
|
|
@@ -4291,7 +4633,7 @@ function getKey(provider) {
|
|
|
4291
4633
|
const row = getDb().prepare("SELECT key_blob FROM provider_keys WHERE provider = ?").get(provider);
|
|
4292
4634
|
if (!row) return null;
|
|
4293
4635
|
try {
|
|
4294
|
-
return
|
|
4636
|
+
return decrypt(row.key_blob);
|
|
4295
4637
|
} catch {
|
|
4296
4638
|
return null;
|
|
4297
4639
|
}
|
|
@@ -4302,7 +4644,7 @@ function setKey(provider, plain) {
|
|
|
4302
4644
|
deleteKey(provider);
|
|
4303
4645
|
return;
|
|
4304
4646
|
}
|
|
4305
|
-
const blob =
|
|
4647
|
+
const blob = encrypt(trimmed);
|
|
4306
4648
|
const now = Date.now();
|
|
4307
4649
|
getDb().prepare(
|
|
4308
4650
|
`INSERT INTO provider_keys (provider, key_blob, created_at, updated_at)
|
|
@@ -4320,8 +4662,10 @@ var PRIMARY_BY_CARRIER = {
|
|
|
4320
4662
|
bai: "haiku-4-5",
|
|
4321
4663
|
anthropic: "haiku-4-5",
|
|
4322
4664
|
openai: "gpt-5-4-mini",
|
|
4323
|
-
google: "gemini-3-1-flash"
|
|
4665
|
+
google: "gemini-3-1-flash",
|
|
4324
4666
|
// xai · no primary (no LLM modelV in registry as of 2026-05-17).
|
|
4667
|
+
moonshot: "kimi-k2-6",
|
|
4668
|
+
zhipu: "glm-5-1"
|
|
4325
4669
|
};
|
|
4326
4670
|
function reachableModelVs() {
|
|
4327
4671
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -4346,6 +4690,7 @@ function reconcileAgentModels(opts = {}) {
|
|
|
4346
4690
|
const carrier = activeCarrier();
|
|
4347
4691
|
const primary = carrier ? PRIMARY_BY_CARRIER[carrier] ?? null : null;
|
|
4348
4692
|
const forcePrimary = opts.forcePrimary === true;
|
|
4693
|
+
const priorCarrier = opts.priorCarrier ?? null;
|
|
4349
4694
|
const switched = [];
|
|
4350
4695
|
const cleared = [];
|
|
4351
4696
|
for (const agent of listAllAgents()) {
|
|
@@ -4353,12 +4698,29 @@ function reconcileAgentModels(opts = {}) {
|
|
|
4353
4698
|
if (agent.carrierPref) {
|
|
4354
4699
|
updateAgent(agent.id, { carrierPref: null });
|
|
4355
4700
|
}
|
|
4701
|
+
if (priorCarrier && v && priorCarrier !== carrier) {
|
|
4702
|
+
writeModelBucketEntry(agent.id, priorCarrier, v);
|
|
4703
|
+
}
|
|
4356
4704
|
if (!forcePrimary && v && reachable.has(v)) continue;
|
|
4357
4705
|
if (primary && carrier) {
|
|
4706
|
+
const bucket = getModelBucket(agent.id);
|
|
4707
|
+
const memorised = bucket[carrier];
|
|
4358
4708
|
const isChair = agent.roleKind === "moderator";
|
|
4359
|
-
|
|
4360
|
-
if (
|
|
4709
|
+
let target;
|
|
4710
|
+
if (memorised && reachable.has(memorised)) {
|
|
4711
|
+
target = memorised;
|
|
4712
|
+
} else {
|
|
4713
|
+
if (memorised) deleteModelBucketEntry(agent.id, carrier);
|
|
4714
|
+
target = isChair ? primary : pickRandomFastModel(carrier) ?? primary;
|
|
4715
|
+
}
|
|
4716
|
+
if (v === target) {
|
|
4717
|
+
if (bucket[carrier] !== target) {
|
|
4718
|
+
writeModelBucketEntry(agent.id, carrier, target);
|
|
4719
|
+
}
|
|
4720
|
+
continue;
|
|
4721
|
+
}
|
|
4361
4722
|
updateAgent(agent.id, { modelV: target });
|
|
4723
|
+
writeModelBucketEntry(agent.id, carrier, target);
|
|
4362
4724
|
switched.push(agent.id);
|
|
4363
4725
|
} else {
|
|
4364
4726
|
if (v === "") continue;
|
|
@@ -4489,8 +4851,15 @@ var FAST_POOL_BY_CARRIER = {
|
|
|
4489
4851
|
],
|
|
4490
4852
|
anthropic: ["opus-4-6-fast", "haiku-4-5"],
|
|
4491
4853
|
openai: ["gpt-5-4-mini"],
|
|
4492
|
-
google: ["gemini-3-flash", "gemini-3-1-flash"]
|
|
4854
|
+
google: ["gemini-3-flash", "gemini-3-1-flash"],
|
|
4493
4855
|
// xai · no fast pool (no LLM modelV in registry).
|
|
4856
|
+
// Moonshot / Zhipu · single-entry pools because the registry only
|
|
4857
|
+
// carries one LLM modelV per provider today. Every director on this
|
|
4858
|
+
// carrier ends up on the same model (no brand variety), which is fine
|
|
4859
|
+
// for these single-model providers · adding more Kimi / GLM rows
|
|
4860
|
+
// to the registry would naturally extend the pool.
|
|
4861
|
+
moonshot: ["kimi-k2-6"],
|
|
4862
|
+
zhipu: ["glm-5-1"]
|
|
4494
4863
|
};
|
|
4495
4864
|
function pickRandomFastModel(carrier) {
|
|
4496
4865
|
if (!carrier) return null;
|
|
@@ -5152,6 +5521,10 @@ function flagshipCandidates() {
|
|
|
5152
5521
|
return out;
|
|
5153
5522
|
}
|
|
5154
5523
|
async function callPhaseLLM(state, modelV, messages, opts) {
|
|
5524
|
+
const r = await callPhaseLLMVerbose(state, modelV, messages, opts);
|
|
5525
|
+
return r ? r.text : null;
|
|
5526
|
+
}
|
|
5527
|
+
async function callPhaseLLMVerbose(state, modelV, messages, opts) {
|
|
5155
5528
|
if (!isModelV(modelV)) return null;
|
|
5156
5529
|
const t = signalWithTimeout(state.controller.signal, LLM_CALL_TIMEOUT_MS);
|
|
5157
5530
|
try {
|
|
@@ -5169,7 +5542,7 @@ async function callPhaseLLM(state, modelV, messages, opts) {
|
|
|
5169
5542
|
return null;
|
|
5170
5543
|
}
|
|
5171
5544
|
}
|
|
5172
|
-
return r.text;
|
|
5545
|
+
return { text: r.text, finishReason: r.finishReason };
|
|
5173
5546
|
} catch (e) {
|
|
5174
5547
|
process.stderr.write(`[persona-builder] ${modelV} failed: ${e instanceof Error ? e.message : String(e)}
|
|
5175
5548
|
`);
|
|
@@ -5347,7 +5720,9 @@ async function runPipeline(state) {
|
|
|
5347
5720
|
if (!profileV1) {
|
|
5348
5721
|
const status = checkAbortOrCap();
|
|
5349
5722
|
if (status === "aborted") return finalizeAbort(state);
|
|
5350
|
-
|
|
5723
|
+
const candidatesTried = flagshipCandidates();
|
|
5724
|
+
const hint = candidatesTried.length === 0 ? "no flagship model is reachable with your current API key \xB7 open Preferences \u25B8 API Key and add a provider that exposes Claude / GPT / Gemini / Kimi / GLM" : `${candidatesTried.length === 1 ? "the only reachable model" : "all reachable flagship models"} (${candidatesTried.join(", ")}) returned an unparseable profile \u2014 try again, or switch to a stronger model via Preferences \u25B8 API Key`;
|
|
5725
|
+
return fail(`Phase 1 (persona spec) failed \xB7 ${hint}.`);
|
|
5351
5726
|
}
|
|
5352
5727
|
partial.profileV1 = profileV1;
|
|
5353
5728
|
partial.spec = toCore(profileV1);
|
|
@@ -5499,17 +5874,58 @@ function finalizeFromCheck(state, status) {
|
|
|
5499
5874
|
}
|
|
5500
5875
|
async function runProfilePass(state, label, webContext) {
|
|
5501
5876
|
const messages = buildAgentProfileMessages({ description: state.description, webContext: webContext ?? null });
|
|
5502
|
-
|
|
5877
|
+
const candidates = flagshipCandidates();
|
|
5878
|
+
if (candidates.length === 0) {
|
|
5879
|
+
process.stderr.write(`[persona-builder/${label}] no reachable models for the active credential \xB7 cannot build profile
|
|
5880
|
+
`);
|
|
5881
|
+
return null;
|
|
5882
|
+
}
|
|
5883
|
+
const PROFILE_ATTEMPTS_PER_MODEL = 2;
|
|
5884
|
+
const PROFILE_MAX_TOKENS_BASE = 4096;
|
|
5885
|
+
const PROFILE_MAX_TOKENS_ESCALATED = 6500;
|
|
5886
|
+
for (const modelV of candidates) {
|
|
5503
5887
|
if (state.controller.signal.aborted) return null;
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5888
|
+
let truncatedLastAttempt = false;
|
|
5889
|
+
for (let attempt = 1; attempt <= PROFILE_ATTEMPTS_PER_MODEL; attempt++) {
|
|
5890
|
+
if (state.controller.signal.aborted) return null;
|
|
5891
|
+
const temperature = attempt === 1 ? 0.6 : 0.8;
|
|
5892
|
+
const maxTokens = truncatedLastAttempt ? PROFILE_MAX_TOKENS_ESCALATED : PROFILE_MAX_TOKENS_BASE;
|
|
5893
|
+
const result = await callPhaseLLMVerbose(state, modelV, messages, { temperature, maxTokens });
|
|
5894
|
+
if (!result || !result.text) {
|
|
5895
|
+
process.stderr.write(`[persona-builder/${label}] ${modelV} attempt ${attempt}/${PROFILE_ATTEMPTS_PER_MODEL} returned no text
|
|
5509
5896
|
`);
|
|
5897
|
+
truncatedLastAttempt = false;
|
|
5898
|
+
continue;
|
|
5899
|
+
}
|
|
5900
|
+
const raw = result.text;
|
|
5901
|
+
const parsed = parseAgentProfile(raw);
|
|
5902
|
+
if (parsed) return parsed;
|
|
5903
|
+
const truncated = result.finishReason === "length" || looksTruncated(raw);
|
|
5904
|
+
truncatedLastAttempt = truncated;
|
|
5905
|
+
const head = raw.slice(0, 200).replace(/\s+/g, " ");
|
|
5906
|
+
const tail = raw.length > 200 ? raw.slice(-160).replace(/\s+/g, " ") : "";
|
|
5907
|
+
process.stderr.write(
|
|
5908
|
+
`[persona-builder/${label}] ${modelV} attempt ${attempt}/${PROFILE_ATTEMPTS_PER_MODEL} returned unparseable profile \xB7 len=${raw.length} finish=${result.finishReason ?? "n/a"} maxTokens=${maxTokens}${truncated ? " TRUNCATED" : ""} \xB7 head: ${head}${tail ? ` \u2026 tail: ${tail}` : ""}
|
|
5909
|
+
`
|
|
5910
|
+
);
|
|
5911
|
+
}
|
|
5510
5912
|
}
|
|
5511
5913
|
return null;
|
|
5512
5914
|
}
|
|
5915
|
+
function looksTruncated(raw) {
|
|
5916
|
+
if (!raw) return false;
|
|
5917
|
+
const fenceMatch = /```(?:json)?\s*([\s\S]*)$/i.exec(raw);
|
|
5918
|
+
const body = fenceMatch ? fenceMatch[1] : raw;
|
|
5919
|
+
if (!body) return false;
|
|
5920
|
+
const start = body.indexOf("{");
|
|
5921
|
+
if (start === -1) return false;
|
|
5922
|
+
let depth = 0;
|
|
5923
|
+
for (let i = start; i < body.length; i++) {
|
|
5924
|
+
if (body[i] === "{") depth++;
|
|
5925
|
+
else if (body[i] === "}") depth--;
|
|
5926
|
+
}
|
|
5927
|
+
return depth > 0;
|
|
5928
|
+
}
|
|
5513
5929
|
async function runReActLoop(state, profileV1, reportProgress) {
|
|
5514
5930
|
const creds = getActiveWebSearchCredentials();
|
|
5515
5931
|
if (!creds) {
|
|
@@ -6155,12 +6571,12 @@ async function generateCelebritySeed(opts) {
|
|
|
6155
6571
|
init_db();
|
|
6156
6572
|
|
|
6157
6573
|
// src/utils/id.ts
|
|
6158
|
-
import { randomBytes as
|
|
6574
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
6159
6575
|
var ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
6160
6576
|
var ALPHABET_LEN = ALPHABET.length;
|
|
6161
6577
|
var MASK = (1 << 5) - 1;
|
|
6162
6578
|
function newId(len = 12) {
|
|
6163
|
-
const bytes =
|
|
6579
|
+
const bytes = randomBytes5(len);
|
|
6164
6580
|
let out = "";
|
|
6165
6581
|
for (let i = 0; i < len; i++) {
|
|
6166
6582
|
out += ALPHABET[bytes[i] & MASK];
|
|
@@ -8109,7 +8525,14 @@ function agentsRouter() {
|
|
|
8109
8525
|
modelV,
|
|
8110
8526
|
messages: profileMessages,
|
|
8111
8527
|
temperature: 0.6,
|
|
8112
|
-
|
|
8528
|
+
// Bumped from 1600 → 4096 · the profile JSON plus any
|
|
8529
|
+
// chain-of-thought / reasoning preamble on Kimi / GLM /
|
|
8530
|
+
// DeepSeek-R lines easily overran the smaller ceiling on
|
|
8531
|
+
// OpenRouter routes, which surface truncation as
|
|
8532
|
+
// "unparseable JSON" (the closing `}` is missing). Quick
|
|
8533
|
+
// mode is single-shot — no retry — so the ceiling has to
|
|
8534
|
+
// be generous up front.
|
|
8535
|
+
maxTokens: 4096,
|
|
8113
8536
|
signal
|
|
8114
8537
|
});
|
|
8115
8538
|
const parsed = parseAgentProfile(raw);
|
|
@@ -8117,6 +8540,8 @@ function agentsRouter() {
|
|
|
8117
8540
|
profile = parsed;
|
|
8118
8541
|
break;
|
|
8119
8542
|
}
|
|
8543
|
+
process.stderr.write(`[agent-spec/profile] ${modelV} returned unparseable profile \xB7 len=${raw.length} head: ${raw.slice(0, 200).replace(/\s+/g, " ")}
|
|
8544
|
+
`);
|
|
8120
8545
|
} catch (e) {
|
|
8121
8546
|
process.stderr.write(`[agent-spec/profile] ${modelV} failed: ${e instanceof Error ? e.message : String(e)}
|
|
8122
8547
|
`);
|
|
@@ -8490,7 +8915,7 @@ function agentsRouter() {
|
|
|
8490
8915
|
patch.carrierPref = null;
|
|
8491
8916
|
} else if (typeof b.carrierPref === "string") {
|
|
8492
8917
|
const v = b.carrierPref.trim();
|
|
8493
|
-
const allowed = /* @__PURE__ */ new Set(["openrouter", "bai", "anthropic", "openai", "google", "xai"]);
|
|
8918
|
+
const allowed = /* @__PURE__ */ new Set(["openrouter", "bai", "anthropic", "openai", "google", "xai", "moonshot", "zhipu"]);
|
|
8494
8919
|
if (!allowed.has(v)) {
|
|
8495
8920
|
return c.json({ error: `unknown carrier: ${v}` }, 400);
|
|
8496
8921
|
}
|
|
@@ -8542,6 +8967,42 @@ function agentsRouter() {
|
|
|
8542
8967
|
patch.isPinned = b.isPinned;
|
|
8543
8968
|
}
|
|
8544
8969
|
const updated = updateAgent(id, patch);
|
|
8970
|
+
if (updated) {
|
|
8971
|
+
if (patch.modelV !== void 0) {
|
|
8972
|
+
const carrier = activeCarrier();
|
|
8973
|
+
if (carrier) {
|
|
8974
|
+
try {
|
|
8975
|
+
writeModelBucketEntry(id, carrier, patch.modelV);
|
|
8976
|
+
} catch (e) {
|
|
8977
|
+
process.stderr.write(
|
|
8978
|
+
`[agents.patch] model bucket write failed for ${id}: ${e instanceof Error ? e.message : String(e)}
|
|
8979
|
+
`
|
|
8980
|
+
);
|
|
8981
|
+
}
|
|
8982
|
+
}
|
|
8983
|
+
}
|
|
8984
|
+
if ("voice" in patch) {
|
|
8985
|
+
if (patch.voice && updated.voice && (updated.voice.provider === "minimax" || updated.voice.provider === "elevenlabs")) {
|
|
8986
|
+
try {
|
|
8987
|
+
writeVoiceBucketEntry(id, updated.voice.provider, updated.voice);
|
|
8988
|
+
} catch (e) {
|
|
8989
|
+
process.stderr.write(
|
|
8990
|
+
`[agents.patch] voice bucket write failed for ${id}: ${e instanceof Error ? e.message : String(e)}
|
|
8991
|
+
`
|
|
8992
|
+
);
|
|
8993
|
+
}
|
|
8994
|
+
} else if (!patch.voice && existing.voice && (existing.voice.provider === "minimax" || existing.voice.provider === "elevenlabs")) {
|
|
8995
|
+
try {
|
|
8996
|
+
deleteVoiceBucketEntry(id, existing.voice.provider);
|
|
8997
|
+
} catch (e) {
|
|
8998
|
+
process.stderr.write(
|
|
8999
|
+
`[agents.patch] voice bucket delete failed for ${id}: ${e instanceof Error ? e.message : String(e)}
|
|
9000
|
+
`
|
|
9001
|
+
);
|
|
9002
|
+
}
|
|
9003
|
+
}
|
|
9004
|
+
}
|
|
9005
|
+
}
|
|
8545
9006
|
return c.json(updated);
|
|
8546
9007
|
});
|
|
8547
9008
|
r.delete("/:id", (c) => {
|
|
@@ -13700,7 +14161,7 @@ function cleanupOrphanedStreams(opts = {}) {
|
|
|
13700
14161
|
|
|
13701
14162
|
// src/storage/rooms.ts
|
|
13702
14163
|
init_db();
|
|
13703
|
-
var ROOM_COLS = "id, number, name, subject, mode, intensity, delivery_mode, vote_trigger, status, brief_style, awaiting_continue, awaiting_clarify, created_at, paused_at, adjourned_at, incognito, parent_room_id, parent_brief_id";
|
|
14164
|
+
var ROOM_COLS = "id, number, name, subject, mode, intensity, delivery_mode, vote_trigger, status, brief_style, awaiting_continue, awaiting_clarify, created_at, paused_at, adjourned_at, incognito, parent_room_id, parent_brief_id, name_auto";
|
|
13704
14165
|
function mapRow8(row) {
|
|
13705
14166
|
return {
|
|
13706
14167
|
id: row.id,
|
|
@@ -13720,7 +14181,8 @@ function mapRow8(row) {
|
|
|
13720
14181
|
adjournedAt: row.adjourned_at,
|
|
13721
14182
|
incognito: row.incognito === 1,
|
|
13722
14183
|
parentRoomId: row.parent_room_id,
|
|
13723
|
-
parentBriefId: row.parent_brief_id
|
|
14184
|
+
parentBriefId: row.parent_brief_id,
|
|
14185
|
+
nameAuto: row.name_auto === 1
|
|
13724
14186
|
};
|
|
13725
14187
|
}
|
|
13726
14188
|
function mapMember(row) {
|
|
@@ -13784,7 +14246,7 @@ function createRoom(input) {
|
|
|
13784
14246
|
const briefStyle = input.briefStyle ?? "auto";
|
|
13785
14247
|
const deliveryMode = input.deliveryMode === "voice" ? "voice" : "text";
|
|
13786
14248
|
const insertRoom = db.prepare(
|
|
13787
|
-
"INSERT INTO rooms (id, number, name, subject, mode, intensity, delivery_mode, brief_style, status, created_at, parent_room_id, parent_brief_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'live', ?, ?, ?)"
|
|
14249
|
+
"INSERT INTO rooms (id, number, name, subject, mode, intensity, delivery_mode, brief_style, status, created_at, parent_room_id, parent_brief_id, name_auto) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'live', ?, ?, ?, ?)"
|
|
13788
14250
|
);
|
|
13789
14251
|
const insertMember = db.prepare(
|
|
13790
14252
|
"INSERT INTO room_members (room_id, agent_id, position, joined_at) VALUES (?, ?, ?, ?)"
|
|
@@ -13792,8 +14254,9 @@ function createRoom(input) {
|
|
|
13792
14254
|
const chair = getChairAgent();
|
|
13793
14255
|
const parentRoomId = input.parentRoomId && input.parentRoomId.trim() ? input.parentRoomId.trim() : null;
|
|
13794
14256
|
const parentBriefId = input.parentBriefId && input.parentBriefId.trim() ? input.parentBriefId.trim() : null;
|
|
14257
|
+
const nameAuto = input.nameAuto === false ? 0 : 1;
|
|
13795
14258
|
const tx = db.transaction(() => {
|
|
13796
|
-
insertRoom.run(id, number, input.name, input.subject, mode, intensity, deliveryMode, briefStyle, now, parentRoomId, parentBriefId);
|
|
14259
|
+
insertRoom.run(id, number, input.name, input.subject, mode, intensity, deliveryMode, briefStyle, now, parentRoomId, parentBriefId, nameAuto);
|
|
13797
14260
|
if (chair) insertMember.run(id, chair.id, -1, now);
|
|
13798
14261
|
input.agentIds.forEach((agentId, idx) => {
|
|
13799
14262
|
if (chair && agentId === chair.id) return;
|
|
@@ -13820,6 +14283,12 @@ function setRoomStatus(roomId, status, ts = {}) {
|
|
|
13820
14283
|
vals.push(roomId);
|
|
13821
14284
|
getDb().prepare(`UPDATE rooms SET ${sets.join(", ")} WHERE id = ?`).run(...vals);
|
|
13822
14285
|
}
|
|
14286
|
+
function setRoomNameFromAuto(roomId, name) {
|
|
14287
|
+
const trimmed = name.trim();
|
|
14288
|
+
if (!trimmed) return false;
|
|
14289
|
+
const r = getDb().prepare("UPDATE rooms SET name = ? WHERE id = ? AND name_auto = 1").run(trimmed, roomId);
|
|
14290
|
+
return r.changes > 0;
|
|
14291
|
+
}
|
|
13823
14292
|
function addRoomMember(roomId, agentId) {
|
|
13824
14293
|
const db = getDb();
|
|
13825
14294
|
const existing = db.prepare("SELECT agent_id, position, joined_at, removed_at FROM room_members WHERE room_id = ? AND agent_id = ?").get(roomId, agentId);
|
|
@@ -14339,6 +14808,103 @@ import { Buffer as Buffer2 } from "buffer";
|
|
|
14339
14808
|
init_db();
|
|
14340
14809
|
import { createHash } from "crypto";
|
|
14341
14810
|
|
|
14811
|
+
// src/storage/voice-credentials.ts
|
|
14812
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
14813
|
+
init_db();
|
|
14814
|
+
var ALL_VOICE_PROVIDERS = ["minimax", "elevenlabs"];
|
|
14815
|
+
function isVoiceProvider(p) {
|
|
14816
|
+
return ALL_VOICE_PROVIDERS.includes(p);
|
|
14817
|
+
}
|
|
14818
|
+
var VOICE_PROVIDER_PRIORITY = ["minimax", "elevenlabs"];
|
|
14819
|
+
function rowToMeta3(row) {
|
|
14820
|
+
if (!isVoiceProvider(row.provider)) return null;
|
|
14821
|
+
let preview = "";
|
|
14822
|
+
try {
|
|
14823
|
+
preview = maskCredential(decryptCredential(row.key_blob));
|
|
14824
|
+
} catch {
|
|
14825
|
+
preview = "";
|
|
14826
|
+
}
|
|
14827
|
+
return {
|
|
14828
|
+
id: row.id,
|
|
14829
|
+
provider: row.provider,
|
|
14830
|
+
label: row.label,
|
|
14831
|
+
preview,
|
|
14832
|
+
createdAt: row.created_at,
|
|
14833
|
+
updatedAt: row.updated_at
|
|
14834
|
+
};
|
|
14835
|
+
}
|
|
14836
|
+
function listVoiceCredentials() {
|
|
14837
|
+
const rows = getDb().prepare("SELECT id, provider, label, key_blob, created_at, updated_at FROM voice_credentials ORDER BY created_at ASC").all();
|
|
14838
|
+
return rows.map(rowToMeta3).filter((m) => m !== null);
|
|
14839
|
+
}
|
|
14840
|
+
function getVoiceCredentialMeta(id) {
|
|
14841
|
+
const row = getDb().prepare("SELECT id, provider, label, key_blob, created_at, updated_at FROM voice_credentials WHERE id = ?").get(id);
|
|
14842
|
+
if (!row) return null;
|
|
14843
|
+
return rowToMeta3(row);
|
|
14844
|
+
}
|
|
14845
|
+
function getVoiceCredentialKey(id) {
|
|
14846
|
+
const row = getDb().prepare("SELECT key_blob FROM voice_credentials WHERE id = ?").get(id);
|
|
14847
|
+
if (!row) return null;
|
|
14848
|
+
try {
|
|
14849
|
+
return decryptCredential(row.key_blob);
|
|
14850
|
+
} catch {
|
|
14851
|
+
return null;
|
|
14852
|
+
}
|
|
14853
|
+
}
|
|
14854
|
+
function providerDisplayName3(provider) {
|
|
14855
|
+
switch (provider) {
|
|
14856
|
+
case "minimax":
|
|
14857
|
+
return "MiniMax";
|
|
14858
|
+
case "elevenlabs":
|
|
14859
|
+
return "ElevenLabs";
|
|
14860
|
+
}
|
|
14861
|
+
}
|
|
14862
|
+
function resolveFreeLabel3(provider, suggested) {
|
|
14863
|
+
const base = suggested && suggested.trim() || providerDisplayName3(provider);
|
|
14864
|
+
const existing = new Set(
|
|
14865
|
+
getDb().prepare("SELECT label FROM voice_credentials").all().map((r) => r.label)
|
|
14866
|
+
);
|
|
14867
|
+
if (!existing.has(base)) return base;
|
|
14868
|
+
for (let n = 2; n < 1e3; n++) {
|
|
14869
|
+
const candidate = `${base} ${n}`;
|
|
14870
|
+
if (!existing.has(candidate)) return candidate;
|
|
14871
|
+
}
|
|
14872
|
+
return `${base} ${Date.now()}`;
|
|
14873
|
+
}
|
|
14874
|
+
function createVoiceCredential(provider, label, plain) {
|
|
14875
|
+
const trimmed = plain.trim();
|
|
14876
|
+
if (!trimmed) return null;
|
|
14877
|
+
if (!isVoiceProvider(provider)) return null;
|
|
14878
|
+
const resolvedLabel = resolveFreeLabel3(provider, label);
|
|
14879
|
+
const id = randomBytes6(8).toString("hex");
|
|
14880
|
+
const blob = encryptCredential(trimmed);
|
|
14881
|
+
const now = Date.now();
|
|
14882
|
+
getDb().prepare(
|
|
14883
|
+
`INSERT INTO voice_credentials (id, provider, label, key_blob, created_at, updated_at)
|
|
14884
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
14885
|
+
).run(id, provider, resolvedLabel, blob, now, now);
|
|
14886
|
+
return getVoiceCredentialMeta(id);
|
|
14887
|
+
}
|
|
14888
|
+
function deleteVoiceCredential(id) {
|
|
14889
|
+
const meta = getVoiceCredentialMeta(id);
|
|
14890
|
+
if (!meta) return null;
|
|
14891
|
+
getDb().prepare("DELETE FROM voice_credentials WHERE id = ?").run(id);
|
|
14892
|
+
return meta.provider;
|
|
14893
|
+
}
|
|
14894
|
+
function resolveActiveVoiceCredential() {
|
|
14895
|
+
const prefs = getPrefs();
|
|
14896
|
+
if (!prefs.activeVoiceCredentialId) return null;
|
|
14897
|
+
return getVoiceCredentialMeta(prefs.activeVoiceCredentialId);
|
|
14898
|
+
}
|
|
14899
|
+
function getActiveVoiceProvider() {
|
|
14900
|
+
return resolveActiveVoiceCredential()?.provider ?? null;
|
|
14901
|
+
}
|
|
14902
|
+
function getActiveVoiceKeyPlaintext() {
|
|
14903
|
+
const active = resolveActiveVoiceCredential();
|
|
14904
|
+
if (!active) return null;
|
|
14905
|
+
return getVoiceCredentialKey(active.id);
|
|
14906
|
+
}
|
|
14907
|
+
|
|
14342
14908
|
// src/voice/registry.ts
|
|
14343
14909
|
function minimaxBaseUrl() {
|
|
14344
14910
|
const region = getPrefs().minimaxRegion;
|
|
@@ -14383,10 +14949,12 @@ function listConfiguredVoices() {
|
|
|
14383
14949
|
const out = [];
|
|
14384
14950
|
const openaiReady = !!getKey("openai");
|
|
14385
14951
|
if (openaiReady) out.push(...OPENAI_VOICES.map((v) => ({ ...v, configured: true })));
|
|
14386
|
-
const
|
|
14387
|
-
if (
|
|
14388
|
-
|
|
14389
|
-
if (
|
|
14952
|
+
const activeProvider = getActiveVoiceProvider();
|
|
14953
|
+
if (activeProvider === "minimax") {
|
|
14954
|
+
out.push(...MINIMAX_SYSTEM_VOICES.map((v) => ({ ...v, configured: true })));
|
|
14955
|
+
} else if (activeProvider === "elevenlabs") {
|
|
14956
|
+
out.push(...ELEVENLABS_DEFAULT_VOICES.map((v) => ({ ...v, configured: true })));
|
|
14957
|
+
}
|
|
14390
14958
|
out.push({
|
|
14391
14959
|
provider: "browser",
|
|
14392
14960
|
model: "speechSynthesis",
|
|
@@ -14397,14 +14965,21 @@ function listConfiguredVoices() {
|
|
|
14397
14965
|
return out;
|
|
14398
14966
|
}
|
|
14399
14967
|
async function listAvailableVoices() {
|
|
14968
|
+
const activeProvider = getActiveVoiceProvider();
|
|
14400
14969
|
let voices = listConfiguredVoices();
|
|
14401
|
-
|
|
14402
|
-
|
|
14403
|
-
|
|
14970
|
+
if (!activeProvider) {
|
|
14971
|
+
return { voices, provider: null, configured: false };
|
|
14972
|
+
}
|
|
14973
|
+
const activeKey = getActiveVoiceKeyPlaintext();
|
|
14974
|
+
if (!activeKey) {
|
|
14975
|
+
return { voices, provider: activeProvider, configured: false };
|
|
14976
|
+
}
|
|
14977
|
+
if (activeProvider === "minimax") {
|
|
14978
|
+
try {
|
|
14404
14979
|
const res = await fetch(`${minimaxBaseUrl()}/v1/get_voice`, {
|
|
14405
14980
|
method: "POST",
|
|
14406
14981
|
headers: {
|
|
14407
|
-
"authorization": `Bearer ${
|
|
14982
|
+
"authorization": `Bearer ${activeKey}`,
|
|
14408
14983
|
"content-type": "application/json"
|
|
14409
14984
|
},
|
|
14410
14985
|
body: JSON.stringify({ voice_type: "all" })
|
|
@@ -14433,9 +15008,9 @@ async function listAvailableVoices() {
|
|
|
14433
15008
|
}
|
|
14434
15009
|
} catch {
|
|
14435
15010
|
}
|
|
15011
|
+
return { voices, provider: "minimax", configured: true };
|
|
14436
15012
|
}
|
|
14437
|
-
|
|
14438
|
-
if (elKey) {
|
|
15013
|
+
if (activeProvider === "elevenlabs") {
|
|
14439
15014
|
const personal = [];
|
|
14440
15015
|
const shared = [];
|
|
14441
15016
|
await Promise.all([
|
|
@@ -14443,7 +15018,7 @@ async function listAvailableVoices() {
|
|
|
14443
15018
|
try {
|
|
14444
15019
|
const res = await fetch(
|
|
14445
15020
|
"https://api.elevenlabs.io/v1/voices?show_legacy=true&include_total_count=true",
|
|
14446
|
-
{ headers: { "xi-api-key":
|
|
15021
|
+
{ headers: { "xi-api-key": activeKey } }
|
|
14447
15022
|
);
|
|
14448
15023
|
if (!res.ok) {
|
|
14449
15024
|
const errText = await res.text();
|
|
@@ -14471,7 +15046,7 @@ async function listAvailableVoices() {
|
|
|
14471
15046
|
try {
|
|
14472
15047
|
const res = await fetch(
|
|
14473
15048
|
"https://api.elevenlabs.io/v1/shared-voices?page_size=100",
|
|
14474
|
-
{ headers: { "xi-api-key":
|
|
15049
|
+
{ headers: { "xi-api-key": activeKey } }
|
|
14475
15050
|
);
|
|
14476
15051
|
if (!res.ok) {
|
|
14477
15052
|
const errText = await res.text();
|
|
@@ -14524,8 +15099,9 @@ async function listAvailableVoices() {
|
|
|
14524
15099
|
}));
|
|
14525
15100
|
voices = [...nonEl, ...personalMapped, ...sharedMapped];
|
|
14526
15101
|
}
|
|
15102
|
+
return { voices, provider: "elevenlabs", configured: true };
|
|
14527
15103
|
}
|
|
14528
|
-
return voices;
|
|
15104
|
+
return { voices, provider: activeProvider, configured: true };
|
|
14529
15105
|
}
|
|
14530
15106
|
function elevenLabsSharedVoiceRows(raw) {
|
|
14531
15107
|
if (!Array.isArray(raw)) return [];
|
|
@@ -14574,6 +15150,10 @@ function defaultVoiceForProvider(provider) {
|
|
|
14574
15150
|
}
|
|
14575
15151
|
|
|
14576
15152
|
// src/voice/tts.ts
|
|
15153
|
+
function activeVoiceKeyFor(wanted) {
|
|
15154
|
+
if (getActiveVoiceProvider() !== wanted) return null;
|
|
15155
|
+
return getActiveVoiceKeyPlaintext();
|
|
15156
|
+
}
|
|
14577
15157
|
function minimaxBaseUrl2() {
|
|
14578
15158
|
const region = getPrefs().minimaxRegion;
|
|
14579
15159
|
return region === "intl" ? "https://api.minimax.io" : "https://api.minimaxi.com";
|
|
@@ -14638,9 +15218,26 @@ function cleanForSpeech(md) {
|
|
|
14638
15218
|
return out.trim();
|
|
14639
15219
|
}
|
|
14640
15220
|
function voiceProfileForAgent(agent) {
|
|
14641
|
-
|
|
15221
|
+
const activeProvider = getActiveVoiceProvider();
|
|
15222
|
+
if (agent.voice && agent.voice.provider === activeProvider) {
|
|
15223
|
+
return agent.voice;
|
|
15224
|
+
}
|
|
15225
|
+
if (agent.voice && activeProvider) {
|
|
15226
|
+
const fresh = defaultVoiceForProvider(activeProvider);
|
|
15227
|
+
if (fresh) {
|
|
15228
|
+
return {
|
|
15229
|
+
provider: fresh.provider,
|
|
15230
|
+
model: fresh.model,
|
|
15231
|
+
voiceId: fresh.voiceId,
|
|
15232
|
+
speed: agent.voice.speed ?? 1,
|
|
15233
|
+
pitch: agent.voice.pitch ?? 0,
|
|
15234
|
+
volume: agent.voice.volume ?? 1,
|
|
15235
|
+
...agent.voice.emotion ? { emotion: agent.voice.emotion } : {}
|
|
15236
|
+
};
|
|
15237
|
+
}
|
|
15238
|
+
}
|
|
14642
15239
|
const fallback = defaultVoiceForProvider(
|
|
14643
|
-
|
|
15240
|
+
activeProvider ?? (getKey("openai") ? "openai" : "browser")
|
|
14644
15241
|
);
|
|
14645
15242
|
return {
|
|
14646
15243
|
provider: fallback?.provider ?? "browser",
|
|
@@ -14663,15 +15260,16 @@ async function synthesizeSpeech(text, profile, signal) {
|
|
|
14663
15260
|
};
|
|
14664
15261
|
}
|
|
14665
15262
|
async function* synthesizeSpeechStream(text, profile, signal) {
|
|
14666
|
-
if (profile.provider === "elevenlabs" &&
|
|
15263
|
+
if (profile.provider === "elevenlabs" && activeVoiceKeyFor("elevenlabs")) {
|
|
14667
15264
|
yield* synthesizeElevenLabsStream(text, profile, signal);
|
|
14668
15265
|
return;
|
|
14669
15266
|
}
|
|
14670
|
-
|
|
15267
|
+
const minimaxKey = activeVoiceKeyFor("minimax");
|
|
15268
|
+
if (profile.provider !== "minimax" || !minimaxKey) {
|
|
14671
15269
|
yield await synthesizeSpeech(text, profile, signal);
|
|
14672
15270
|
return;
|
|
14673
15271
|
}
|
|
14674
|
-
const key =
|
|
15272
|
+
const key = minimaxKey;
|
|
14675
15273
|
const model = profile.model || "speech-2.8-hd";
|
|
14676
15274
|
const res = await fetch(`${minimaxBaseUrl2()}/v1/t2a_v2`, {
|
|
14677
15275
|
method: "POST",
|
|
@@ -14787,7 +15385,7 @@ async function* synthesizeSpeechStream(text, profile, signal) {
|
|
|
14787
15385
|
}
|
|
14788
15386
|
}
|
|
14789
15387
|
async function synthesizeMiniMax(text, profile, signal) {
|
|
14790
|
-
const key =
|
|
15388
|
+
const key = activeVoiceKeyFor("minimax");
|
|
14791
15389
|
if (!key) {
|
|
14792
15390
|
return { provider: "browser", model: "speechSynthesis", voiceId: "system-default", text };
|
|
14793
15391
|
}
|
|
@@ -14897,7 +15495,7 @@ async function synthesizeOpenAI(text, profile, signal) {
|
|
|
14897
15495
|
};
|
|
14898
15496
|
}
|
|
14899
15497
|
async function synthesizeElevenLabs(text, profile, signal) {
|
|
14900
|
-
const key =
|
|
15498
|
+
const key = activeVoiceKeyFor("elevenlabs");
|
|
14901
15499
|
if (!key) {
|
|
14902
15500
|
return { provider: "browser", model: "speechSynthesis", voiceId: "system-default", text };
|
|
14903
15501
|
}
|
|
@@ -14942,7 +15540,7 @@ async function synthesizeElevenLabs(text, profile, signal) {
|
|
|
14942
15540
|
};
|
|
14943
15541
|
}
|
|
14944
15542
|
async function* synthesizeElevenLabsStream(text, profile, signal) {
|
|
14945
|
-
const key =
|
|
15543
|
+
const key = activeVoiceKeyFor("elevenlabs");
|
|
14946
15544
|
if (!key) {
|
|
14947
15545
|
yield await synthesizeSpeech(text, profile, signal);
|
|
14948
15546
|
return;
|
|
@@ -16906,6 +17504,7 @@ function credentialsRouter() {
|
|
|
16906
17504
|
} else {
|
|
16907
17505
|
return c.json({ error: "id must be a string or null" }, 400);
|
|
16908
17506
|
}
|
|
17507
|
+
const priorCarrier = activeCarrier();
|
|
16909
17508
|
if (nextId) {
|
|
16910
17509
|
const meta = getLlmCredentialMeta(nextId);
|
|
16911
17510
|
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
@@ -16916,7 +17515,7 @@ function credentialsRouter() {
|
|
|
16916
17515
|
updatePrefs({ activeLlmCredentialId: null });
|
|
16917
17516
|
}
|
|
16918
17517
|
try {
|
|
16919
|
-
reconcileAgentModels({ forcePrimary: true });
|
|
17518
|
+
reconcileAgentModels({ forcePrimary: true, priorCarrier });
|
|
16920
17519
|
} catch (e) {
|
|
16921
17520
|
process.stderr.write(`[credentials.active] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
16922
17521
|
`);
|
|
@@ -16948,7 +17547,7 @@ function credentialsRouter() {
|
|
|
16948
17547
|
const flagship = PRIMARY_BY_CARRIER[provider];
|
|
16949
17548
|
if (flagship) updatePrefs({ defaultModelV: flagship });
|
|
16950
17549
|
try {
|
|
16951
|
-
reconcileAgentModels({ forcePrimary: true });
|
|
17550
|
+
reconcileAgentModels({ forcePrimary: true, priorCarrier: null });
|
|
16952
17551
|
} catch (e) {
|
|
16953
17552
|
process.stderr.write(`[credentials.post] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
16954
17553
|
`);
|
|
@@ -16963,6 +17562,7 @@ function credentialsRouter() {
|
|
|
16963
17562
|
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
16964
17563
|
const prefs = getPrefs();
|
|
16965
17564
|
const wasActive = prefs.activeLlmCredentialId === id;
|
|
17565
|
+
const priorCarrier = wasActive ? activeCarrier() : null;
|
|
16966
17566
|
const removedProvider = deleteLlmCredential(id);
|
|
16967
17567
|
if (wasActive) {
|
|
16968
17568
|
const nextId = pickNextActiveId(removedProvider);
|
|
@@ -16978,7 +17578,7 @@ function credentialsRouter() {
|
|
|
16978
17578
|
}
|
|
16979
17579
|
}
|
|
16980
17580
|
try {
|
|
16981
|
-
reconcileAgentModels(wasActive ? { forcePrimary: true } : void 0);
|
|
17581
|
+
reconcileAgentModels(wasActive ? { forcePrimary: true, priorCarrier } : void 0);
|
|
16982
17582
|
} catch (e) {
|
|
16983
17583
|
process.stderr.write(`[credentials.delete] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
16984
17584
|
`);
|
|
@@ -17004,39 +17604,6 @@ var PROVIDERS = /* @__PURE__ */ new Set([
|
|
|
17004
17604
|
function isProvider(s) {
|
|
17005
17605
|
return PROVIDERS.has(s);
|
|
17006
17606
|
}
|
|
17007
|
-
function autoAssignVoicesOnFirstKey(provider) {
|
|
17008
|
-
if (provider !== "minimax" && provider !== "elevenlabs") return;
|
|
17009
|
-
const pool = listConfiguredVoices().filter((v) => v.provider === provider);
|
|
17010
|
-
if (pool.length === 0) return;
|
|
17011
|
-
const agents = listAllAgents();
|
|
17012
|
-
if (agents.length === 0) return;
|
|
17013
|
-
const shuffled = [...pool];
|
|
17014
|
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
17015
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
17016
|
-
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
17017
|
-
}
|
|
17018
|
-
for (let i = 0; i < agents.length; i++) {
|
|
17019
|
-
const v = shuffled[i % shuffled.length];
|
|
17020
|
-
const prev = agents[i].voice;
|
|
17021
|
-
const profile = {
|
|
17022
|
-
provider: v.provider,
|
|
17023
|
-
model: v.model,
|
|
17024
|
-
voiceId: v.voiceId,
|
|
17025
|
-
...prev?.speed !== void 0 ? { speed: prev.speed } : {},
|
|
17026
|
-
...prev?.pitch !== void 0 ? { pitch: prev.pitch } : {},
|
|
17027
|
-
...prev?.volume !== void 0 ? { volume: prev.volume } : {},
|
|
17028
|
-
...prev?.emotion !== void 0 ? { emotion: prev.emotion } : {}
|
|
17029
|
-
};
|
|
17030
|
-
try {
|
|
17031
|
-
updateAgent(agents[i].id, { voice: profile });
|
|
17032
|
-
} catch (e) {
|
|
17033
|
-
process.stderr.write(
|
|
17034
|
-
`[keys.put] auto-assign voice failed for ${agents[i].id}: ${e instanceof Error ? e.message : String(e)}
|
|
17035
|
-
`
|
|
17036
|
-
);
|
|
17037
|
-
}
|
|
17038
|
-
}
|
|
17039
|
-
}
|
|
17040
17607
|
function keysRouter() {
|
|
17041
17608
|
const r = new Hono5();
|
|
17042
17609
|
r.get("/", (c) => {
|
|
@@ -17062,6 +17629,12 @@ function keysRouter() {
|
|
|
17062
17629
|
if (isLlmProvider(provider)) {
|
|
17063
17630
|
return c.json({ error: "LLM providers use POST /api/credentials" }, 410);
|
|
17064
17631
|
}
|
|
17632
|
+
if (provider === "minimax" || provider === "elevenlabs") {
|
|
17633
|
+
return c.json({ error: "voice providers use POST /api/voice-credentials" }, 410);
|
|
17634
|
+
}
|
|
17635
|
+
if (provider === "brave" || provider === "tavily") {
|
|
17636
|
+
return c.json({ error: "search providers use POST /api/search-credentials" }, 410);
|
|
17637
|
+
}
|
|
17065
17638
|
let body;
|
|
17066
17639
|
try {
|
|
17067
17640
|
body = await c.req.json();
|
|
@@ -17070,16 +17643,7 @@ function keysRouter() {
|
|
|
17070
17643
|
}
|
|
17071
17644
|
const key = body?.key;
|
|
17072
17645
|
if (typeof key !== "string") return c.json({ error: "body must contain { key: string }" }, 400);
|
|
17073
|
-
const hadAnyVoiceKeyBefore = !!getKey("minimax") || !!getKey("elevenlabs");
|
|
17074
17646
|
setKey(provider, key);
|
|
17075
|
-
if ((provider === "minimax" || provider === "elevenlabs") && key.trim().length > 0 && !hadAnyVoiceKeyBefore) {
|
|
17076
|
-
try {
|
|
17077
|
-
autoAssignVoicesOnFirstKey(provider);
|
|
17078
|
-
} catch (e) {
|
|
17079
|
-
process.stderr.write(`[keys.put] voice auto-assign failed: ${e instanceof Error ? e.message : String(e)}
|
|
17080
|
-
`);
|
|
17081
|
-
}
|
|
17082
|
-
}
|
|
17083
17647
|
const fresh = listKeyMeta().find((m) => m.provider === provider);
|
|
17084
17648
|
return c.json(
|
|
17085
17649
|
fresh ?? { provider, configured: key.trim().length > 0, updatedAt: Date.now(), preview: null }
|
|
@@ -17091,6 +17655,12 @@ function keysRouter() {
|
|
|
17091
17655
|
if (isLlmProvider(provider)) {
|
|
17092
17656
|
return c.json({ error: "LLM providers use DELETE /api/credentials/:id" }, 410);
|
|
17093
17657
|
}
|
|
17658
|
+
if (provider === "minimax" || provider === "elevenlabs") {
|
|
17659
|
+
return c.json({ error: "voice providers use DELETE /api/voice-credentials/:id" }, 410);
|
|
17660
|
+
}
|
|
17661
|
+
if (provider === "brave" || provider === "tavily") {
|
|
17662
|
+
return c.json({ error: "search providers use DELETE /api/search-credentials/:id" }, 410);
|
|
17663
|
+
}
|
|
17094
17664
|
deleteKey(provider);
|
|
17095
17665
|
return c.json({ provider, configured: false, updatedAt: null, preview: null });
|
|
17096
17666
|
});
|
|
@@ -21895,6 +22465,7 @@ async function streamChairMessage(args) {
|
|
|
21895
22465
|
const metaKind = meta?.kind;
|
|
21896
22466
|
const isRoundEndVoice = voiceMode && metaKind === "round-end";
|
|
21897
22467
|
let pingDone = false;
|
|
22468
|
+
let tailDone = false;
|
|
21898
22469
|
let voiceBuf = "";
|
|
21899
22470
|
const voiceProfile = voiceMode ? voiceProfileForAgent(chair) : null;
|
|
21900
22471
|
let voiceSeq = 0;
|
|
@@ -21956,20 +22527,31 @@ async function streamChairMessage(args) {
|
|
|
21956
22527
|
});
|
|
21957
22528
|
if (voiceChunker) {
|
|
21958
22529
|
if (isRoundEndVoice) {
|
|
21959
|
-
|
|
21960
|
-
|
|
21961
|
-
|
|
21962
|
-
if (
|
|
21963
|
-
|
|
21964
|
-
|
|
21965
|
-
|
|
22530
|
+
if (tailDone) {
|
|
22531
|
+
} else {
|
|
22532
|
+
voiceBuf += chunk.delta;
|
|
22533
|
+
if (!pingDone) {
|
|
22534
|
+
const idx = voiceBuf.search(/POINTS\s*:/i);
|
|
22535
|
+
if (idx >= 0) {
|
|
22536
|
+
pingDone = true;
|
|
22537
|
+
const after = voiceBuf.slice(voiceBuf.search(/POINTS\s*:/i));
|
|
22538
|
+
voiceBuf = after.replace(/POINTS\s*:/i, "");
|
|
22539
|
+
}
|
|
21966
22540
|
}
|
|
21967
|
-
|
|
21968
|
-
|
|
21969
|
-
|
|
21970
|
-
|
|
21971
|
-
|
|
21972
|
-
|
|
22541
|
+
if (pingDone && voiceBuf) {
|
|
22542
|
+
const tailIdx = voiceBuf.search(/MODE[-\s]?SHIFT\s*:/i);
|
|
22543
|
+
if (tailIdx >= 0) {
|
|
22544
|
+
tailDone = true;
|
|
22545
|
+
const beforeTail = voiceBuf.slice(0, tailIdx);
|
|
22546
|
+
voiceBuf = beforeTail;
|
|
22547
|
+
}
|
|
22548
|
+
if (voiceBuf) {
|
|
22549
|
+
const cleaned = voiceBuf.replace(/(^|\n)\s*[-*]\s*/g, ". ").replace(/\bBECAUSE\s*:\s*/gi, "");
|
|
22550
|
+
voiceBuf = "";
|
|
22551
|
+
for (const spoken of voiceChunker.push(cleaned)) {
|
|
22552
|
+
await emitChairVoice(spoken);
|
|
22553
|
+
}
|
|
22554
|
+
}
|
|
21973
22555
|
}
|
|
21974
22556
|
}
|
|
21975
22557
|
} else {
|
|
@@ -23110,6 +23692,129 @@ async function pickDirectors(opts) {
|
|
|
23110
23692
|
};
|
|
23111
23693
|
}
|
|
23112
23694
|
|
|
23695
|
+
// src/orchestrator/roomTitle.ts
|
|
23696
|
+
var MAX_TITLE_CHARS = 32;
|
|
23697
|
+
var REJECT_PHRASES = /* @__PURE__ */ new Set([
|
|
23698
|
+
"untitled",
|
|
23699
|
+
"untitled room",
|
|
23700
|
+
"discussion",
|
|
23701
|
+
"chat",
|
|
23702
|
+
"conversation",
|
|
23703
|
+
"topic",
|
|
23704
|
+
"summary",
|
|
23705
|
+
"\u672A\u547D\u540D",
|
|
23706
|
+
"\u8BA8\u8BBA",
|
|
23707
|
+
"\u5BF9\u8BDD",
|
|
23708
|
+
"\u804A\u5929"
|
|
23709
|
+
]);
|
|
23710
|
+
async function generateRoomTitle(roomId) {
|
|
23711
|
+
const room = getRoom(roomId);
|
|
23712
|
+
if (!room) return { kind: "skipped", reason: "no-room" };
|
|
23713
|
+
if (!room.nameAuto) return { kind: "skipped", reason: "user-named" };
|
|
23714
|
+
const subject = room.subject.trim();
|
|
23715
|
+
if (!subject) return { kind: "skipped", reason: "no-subject" };
|
|
23716
|
+
const fallbackName = room.subject.slice(0, 60);
|
|
23717
|
+
if (room.name !== fallbackName) {
|
|
23718
|
+
return { kind: "skipped", reason: "already-renamed", detail: room.name.slice(0, 60) };
|
|
23719
|
+
}
|
|
23720
|
+
const modelV = utilityModelFor();
|
|
23721
|
+
if (!modelV) return { kind: "skipped", reason: "no-model" };
|
|
23722
|
+
const prompt = `You are titling a conversation for a sidebar entry, the way ChatGPT does it. Read the user's opening question and write the title that another reader would expect to see for THIS conversation \u2014 specific enough to distinguish it from any neighbouring entry in the same domain.
|
|
23723
|
+
|
|
23724
|
+
How to write a representative title:
|
|
23725
|
+
1. Identify the CORE SUBJECT or TASK (the noun, the deliverable, the decision being made).
|
|
23726
|
+
2. Strip throat-clearing, polite framing, self-introduction, and product names that are not the subject itself.
|
|
23727
|
+
3. Keep one distinguishing modifier when the bare noun would be ambiguous ("\u4EA7\u54C1\u5BA3\u4F20\u89C6\u9891\u811A\u672C" beats "\u89C6\u9891\u811A\u672C"; "LoRA vs \u5168\u91CF\u5FAE\u8C03" beats "LoRA").
|
|
23728
|
+
4. Use the SAME language as the opening question.
|
|
23729
|
+
|
|
23730
|
+
Length:
|
|
23731
|
+
- Chinese / Japanese: 5-10 characters.
|
|
23732
|
+
- English / Spanish / other Latin scripts: 3-6 words.
|
|
23733
|
+
- \u226424 characters total.
|
|
23734
|
+
|
|
23735
|
+
Format:
|
|
23736
|
+
- Output ONLY the title \u2014 no quotes, no brackets, no trailing punctuation, no labels like "Topic:" / "\u4E3B\u9898\uFF1A", no explanation.
|
|
23737
|
+
- Never output fillers like "Untitled", "Discussion", "Chat", "Conversation", "\u8BA8\u8BBA", "\u5BF9\u8BDD", "\u804A\u5929".
|
|
23738
|
+
|
|
23739
|
+
Examples:
|
|
23740
|
+
|
|
23741
|
+
Input: privateboard.ai \u662F\u6211\u7684\u521B\u4E1A\u4EA7\u54C1\uFF0C\u6211\u73B0\u5728\u60F3\u526A\u8F91\u4E00\u4E2A\u89C6\u9891\u653E\u5230\u5B98\u7F51\u548C x.com \u4E0A\u9762\u5BA3\u4F20\u4ECB\u7ECD\u4EA7\u54C1\u3002\u4F60\u5E2E\u6211\u5199\u4E00\u4E2A\u811A\u672C\u3002
|
|
23742
|
+
Output: \u4EA7\u54C1\u5BA3\u4F20\u89C6\u9891\u811A\u672C
|
|
23743
|
+
|
|
23744
|
+
Input: \u6211\u4EEC\u516C\u53F8\u5728\u8003\u8651\u8981\u4E0D\u8981\u4ECE Postgres \u8FC1\u79FB\u5230 ClickHouse \u5904\u7406\u5206\u6790\u67E5\u8BE2\uFF0C\u80FD\u5E2E\u6211\u5217\u51FA\u6743\u8861\u4E48
|
|
23745
|
+
Output: Postgres \u8F6C ClickHouse \u6743\u8861
|
|
23746
|
+
|
|
23747
|
+
Input: \u6211\u60F3\u8BA8\u8BBA\u4E00\u4E0B LoRA \u5FAE\u8C03\u76F8\u6BD4\u5168\u91CF\u5FAE\u8C03\u6709\u4EC0\u4E48\u4F18\u7F3A\u70B9\uFF0C\u7279\u522B\u662F\u5728\u5C0F\u6A21\u578B\u4E0A
|
|
23748
|
+
Output: LoRA vs \u5168\u91CF\u5FAE\u8C03
|
|
23749
|
+
|
|
23750
|
+
Input: Can you help me debug this Python regex that's failing on Unicode strings with combining marks?
|
|
23751
|
+
Output: Python regex Unicode bug
|
|
23752
|
+
|
|
23753
|
+
Input: I want to redesign our onboarding email sequence \u2014 currently 5 emails over 2 weeks, low click-through.
|
|
23754
|
+
Output: Onboarding email redesign
|
|
23755
|
+
|
|
23756
|
+
--- User's opening question ---
|
|
23757
|
+
${subject}
|
|
23758
|
+
|
|
23759
|
+
--- Title ---
|
|
23760
|
+
`;
|
|
23761
|
+
let raw = "";
|
|
23762
|
+
try {
|
|
23763
|
+
raw = await callLLM({
|
|
23764
|
+
modelV,
|
|
23765
|
+
carrier: null,
|
|
23766
|
+
messages: [{ role: "user", content: prompt }],
|
|
23767
|
+
// Low but not zero · 0.2 was deterministic-ish but kept locking
|
|
23768
|
+
// onto a generic first-noun pick. 0.4 lets the model trade off
|
|
23769
|
+
// alternatives without wandering into creative territory.
|
|
23770
|
+
temperature: 0.4,
|
|
23771
|
+
// 40 was tight enough that a model thinking briefly before
|
|
23772
|
+
// answering would get cut off mid-title; 80 fits the title plus
|
|
23773
|
+
// a small margin without inviting paragraphs.
|
|
23774
|
+
maxTokens: 80
|
|
23775
|
+
});
|
|
23776
|
+
} catch (e) {
|
|
23777
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
23778
|
+
process.stderr.write(`[room-title] LLM call failed for ${roomId}: ${detail}
|
|
23779
|
+
`);
|
|
23780
|
+
return { kind: "skipped", reason: "llm-error", detail };
|
|
23781
|
+
}
|
|
23782
|
+
if (!raw.trim()) {
|
|
23783
|
+
return { kind: "skipped", reason: "empty-output", detail: `model=${modelV}` };
|
|
23784
|
+
}
|
|
23785
|
+
const phrase = sanitiseTitle(raw);
|
|
23786
|
+
if (!phrase) {
|
|
23787
|
+
return { kind: "skipped", reason: "rejected-generic", detail: raw.trim().slice(0, 80) };
|
|
23788
|
+
}
|
|
23789
|
+
const updated = setRoomNameFromAuto(roomId, phrase);
|
|
23790
|
+
if (!updated) return { kind: "skipped", reason: "race-after-rename" };
|
|
23791
|
+
roomBus.emit(roomId, {
|
|
23792
|
+
type: "config-event",
|
|
23793
|
+
kind: "settings-changed",
|
|
23794
|
+
payload: { changes: { name: { from: room.name, to: phrase } } },
|
|
23795
|
+
createdAt: Date.now()
|
|
23796
|
+
});
|
|
23797
|
+
return { kind: "ok", before: room.name, after: phrase };
|
|
23798
|
+
}
|
|
23799
|
+
function sanitiseTitle(raw) {
|
|
23800
|
+
let s = raw.trim();
|
|
23801
|
+
if (!s) return null;
|
|
23802
|
+
const firstLine = s.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
|
|
23803
|
+
if (firstLine) s = firstLine;
|
|
23804
|
+
s = s.replace(/^\s*output\s*[::]\s*/i, "");
|
|
23805
|
+
s = s.replace(/^\s*(topic|title|主题|主題|标题|標題|タイトル)\s*[::]\s*/i, "");
|
|
23806
|
+
s = s.replace(/^[\s"'`「『《【〈“‘]+/, "").replace(/[\s"'`」』》】〉”’]+$/, "");
|
|
23807
|
+
s = s.replace(/[\s.。!!??,,;;::]+$/, "");
|
|
23808
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
23809
|
+
if (!s) return null;
|
|
23810
|
+
if (REJECT_PHRASES.has(s.toLowerCase())) return null;
|
|
23811
|
+
const cps = Array.from(s);
|
|
23812
|
+
if (cps.length > MAX_TITLE_CHARS) {
|
|
23813
|
+
s = cps.slice(0, MAX_TITLE_CHARS).join("");
|
|
23814
|
+
}
|
|
23815
|
+
return s;
|
|
23816
|
+
}
|
|
23817
|
+
|
|
23113
23818
|
// src/routes/rooms.ts
|
|
23114
23819
|
async function runAutoPickAndSeat(roomId, subject) {
|
|
23115
23820
|
const candidates = listAgents().filter((a) => a.roleKind === "director");
|
|
@@ -23229,7 +23934,10 @@ function roomsRouter() {
|
|
|
23229
23934
|
);
|
|
23230
23935
|
}
|
|
23231
23936
|
}
|
|
23232
|
-
const
|
|
23937
|
+
const rawName = typeof b.name === "string" ? b.name.trim() : "";
|
|
23938
|
+
const hasExplicitName = rawName.length > 0;
|
|
23939
|
+
const name = hasExplicitName ? rawName.slice(0, 80) : subject.slice(0, 60);
|
|
23940
|
+
const nameAuto = !hasExplicitName;
|
|
23233
23941
|
const ALLOWED_MODES = /* @__PURE__ */ new Set(["brainstorm", "constructive", "research", "debate", "critique"]);
|
|
23234
23942
|
const ALLOWED_INTENSITY = /* @__PURE__ */ new Set(["calm", "sharp", "terse", "brutal"]);
|
|
23235
23943
|
const normalizeIntensityValue = (v) => v === "brutal" ? "terse" : v;
|
|
@@ -23252,7 +23960,8 @@ function roomsRouter() {
|
|
|
23252
23960
|
deliveryMode,
|
|
23253
23961
|
agentIds,
|
|
23254
23962
|
parentRoomId,
|
|
23255
|
-
parentBriefId
|
|
23963
|
+
parentBriefId,
|
|
23964
|
+
nameAuto
|
|
23256
23965
|
});
|
|
23257
23966
|
insertConfigEvent({
|
|
23258
23967
|
roomId: room.id,
|
|
@@ -23278,6 +23987,25 @@ function roomsRouter() {
|
|
|
23278
23987
|
createdAt: opening.createdAt
|
|
23279
23988
|
});
|
|
23280
23989
|
roomBus.emit(room.id, { type: "message-final", messageId: opening.id });
|
|
23990
|
+
generateRoomTitle(room.id).then((result) => {
|
|
23991
|
+
if (result.kind === "ok") {
|
|
23992
|
+
process.stderr.write(
|
|
23993
|
+
`[room-title] room=${room.id} renamed "${result.before.slice(0, 40)}" \u2192 "${result.after}"
|
|
23994
|
+
`
|
|
23995
|
+
);
|
|
23996
|
+
} else {
|
|
23997
|
+
const tail = result.detail ? ` \xB7 detail=${result.detail.slice(0, 100)}` : "";
|
|
23998
|
+
process.stderr.write(
|
|
23999
|
+
`[room-title] room=${room.id} skipped \xB7 reason=${result.reason}${tail}
|
|
24000
|
+
`
|
|
24001
|
+
);
|
|
24002
|
+
}
|
|
24003
|
+
}).catch((e) => {
|
|
24004
|
+
process.stderr.write(
|
|
24005
|
+
`[room-title] room=${room.id} threw: ${e instanceof Error ? e.message : String(e)}
|
|
24006
|
+
`
|
|
24007
|
+
);
|
|
24008
|
+
});
|
|
23281
24009
|
setAwaitingClarify(room.id, true);
|
|
23282
24010
|
if (mode === "research" && !hasWebSearchKey()) {
|
|
23283
24011
|
const langGuess = /[一-鿿]/.test(subject) ? "zh" : "en";
|
|
@@ -24120,10 +24848,122 @@ function buildRoomExportMarkdown(opts) {
|
|
|
24120
24848
|
return [...headerLines, ...transcriptLines, ...briefsLines].join("\n") + "\n";
|
|
24121
24849
|
}
|
|
24122
24850
|
|
|
24123
|
-
// src/routes/search.ts
|
|
24851
|
+
// src/routes/search-credentials.ts
|
|
24124
24852
|
import { Hono as Hono10 } from "hono";
|
|
24125
|
-
function
|
|
24853
|
+
function payloadFor2(meta, activeId) {
|
|
24854
|
+
return {
|
|
24855
|
+
id: meta.id,
|
|
24856
|
+
provider: meta.provider,
|
|
24857
|
+
label: meta.label,
|
|
24858
|
+
preview: meta.preview,
|
|
24859
|
+
createdAt: meta.createdAt,
|
|
24860
|
+
updatedAt: meta.updatedAt,
|
|
24861
|
+
isActive: meta.id === activeId
|
|
24862
|
+
};
|
|
24863
|
+
}
|
|
24864
|
+
function pickNextActiveSearchId(removedProvider) {
|
|
24865
|
+
const all = listSearchCredentials();
|
|
24866
|
+
if (all.length === 0) return null;
|
|
24867
|
+
if (removedProvider) {
|
|
24868
|
+
const sameProvider = all.filter((c) => c.provider === removedProvider);
|
|
24869
|
+
if (sameProvider.length > 0) {
|
|
24870
|
+
sameProvider.sort((a, b) => a.createdAt - b.createdAt);
|
|
24871
|
+
return sameProvider[0].id;
|
|
24872
|
+
}
|
|
24873
|
+
}
|
|
24874
|
+
const sorted = all.slice().sort((a, b) => {
|
|
24875
|
+
const ai = SEARCH_PROVIDER_PRIORITY.indexOf(a.provider);
|
|
24876
|
+
const bi = SEARCH_PROVIDER_PRIORITY.indexOf(b.provider);
|
|
24877
|
+
if (ai !== bi) return ai - bi;
|
|
24878
|
+
return a.createdAt - b.createdAt;
|
|
24879
|
+
});
|
|
24880
|
+
return sorted[0]?.id ?? null;
|
|
24881
|
+
}
|
|
24882
|
+
function searchCredentialsRouter() {
|
|
24126
24883
|
const r = new Hono10();
|
|
24884
|
+
r.get("/", (c) => {
|
|
24885
|
+
const activeId = getPrefs().activeSearchCredentialId;
|
|
24886
|
+
const items = listSearchCredentials().map((m) => payloadFor2(m, activeId));
|
|
24887
|
+
return c.json({
|
|
24888
|
+
credentials: items,
|
|
24889
|
+
activeId
|
|
24890
|
+
});
|
|
24891
|
+
});
|
|
24892
|
+
r.put("/active", async (c) => {
|
|
24893
|
+
let body;
|
|
24894
|
+
try {
|
|
24895
|
+
body = await c.req.json();
|
|
24896
|
+
} catch {
|
|
24897
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
24898
|
+
}
|
|
24899
|
+
const rawId = body?.id;
|
|
24900
|
+
let nextId;
|
|
24901
|
+
if (rawId === null || rawId === void 0) {
|
|
24902
|
+
nextId = null;
|
|
24903
|
+
} else if (typeof rawId === "string") {
|
|
24904
|
+
nextId = rawId;
|
|
24905
|
+
} else {
|
|
24906
|
+
return c.json({ error: "id must be a string or null" }, 400);
|
|
24907
|
+
}
|
|
24908
|
+
if (nextId) {
|
|
24909
|
+
const meta = getSearchCredentialMeta(nextId);
|
|
24910
|
+
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
24911
|
+
updatePrefs({ activeSearchCredentialId: nextId });
|
|
24912
|
+
} else {
|
|
24913
|
+
updatePrefs({ activeSearchCredentialId: null });
|
|
24914
|
+
}
|
|
24915
|
+
return c.json({ activeId: nextId });
|
|
24916
|
+
});
|
|
24917
|
+
r.post("/", async (c) => {
|
|
24918
|
+
let body;
|
|
24919
|
+
try {
|
|
24920
|
+
body = await c.req.json();
|
|
24921
|
+
} catch {
|
|
24922
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
24923
|
+
}
|
|
24924
|
+
const provider = body?.provider;
|
|
24925
|
+
const labelRaw = body?.label;
|
|
24926
|
+
const key = body?.key;
|
|
24927
|
+
if (typeof provider !== "string" || !isSearchProvider(provider)) {
|
|
24928
|
+
return c.json({ error: "provider must be 'brave' or 'tavily'" }, 400);
|
|
24929
|
+
}
|
|
24930
|
+
if (typeof key !== "string" || key.trim().length === 0) {
|
|
24931
|
+
return c.json({ error: "key must be a non-empty string" }, 400);
|
|
24932
|
+
}
|
|
24933
|
+
const label = typeof labelRaw === "string" ? labelRaw : null;
|
|
24934
|
+
const meta = createSearchCredential(provider, label, key);
|
|
24935
|
+
if (!meta) return c.json({ error: "failed to create credential" }, 500);
|
|
24936
|
+
const hadActive = !!getPrefs().activeSearchCredentialId;
|
|
24937
|
+
if (!hadActive) {
|
|
24938
|
+
updatePrefs({ activeSearchCredentialId: meta.id });
|
|
24939
|
+
}
|
|
24940
|
+
const activeId = getPrefs().activeSearchCredentialId;
|
|
24941
|
+
return c.json(payloadFor2(meta, activeId), 201);
|
|
24942
|
+
});
|
|
24943
|
+
r.delete("/:id", (c) => {
|
|
24944
|
+
const id = c.req.param("id");
|
|
24945
|
+
const meta = getSearchCredentialMeta(id);
|
|
24946
|
+
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
24947
|
+
const prefs = getPrefs();
|
|
24948
|
+
const wasActive = prefs.activeSearchCredentialId === id;
|
|
24949
|
+
const removedProvider = deleteSearchCredential(id);
|
|
24950
|
+
if (wasActive) {
|
|
24951
|
+
const nextId = pickNextActiveSearchId(removedProvider);
|
|
24952
|
+
updatePrefs({ activeSearchCredentialId: nextId });
|
|
24953
|
+
}
|
|
24954
|
+
return c.json({
|
|
24955
|
+
id,
|
|
24956
|
+
deleted: true,
|
|
24957
|
+
activeId: getPrefs().activeSearchCredentialId
|
|
24958
|
+
});
|
|
24959
|
+
});
|
|
24960
|
+
return r;
|
|
24961
|
+
}
|
|
24962
|
+
|
|
24963
|
+
// src/routes/search.ts
|
|
24964
|
+
import { Hono as Hono11 } from "hono";
|
|
24965
|
+
function searchRouter() {
|
|
24966
|
+
const r = new Hono11();
|
|
24127
24967
|
r.get("/", (c) => {
|
|
24128
24968
|
const q = (c.req.query("q") || "").trim();
|
|
24129
24969
|
if (q.length < 1) {
|
|
@@ -24162,7 +25002,7 @@ function searchRouter() {
|
|
|
24162
25002
|
}
|
|
24163
25003
|
|
|
24164
25004
|
// src/routes/usage.ts
|
|
24165
|
-
import { Hono as
|
|
25005
|
+
import { Hono as Hono12 } from "hono";
|
|
24166
25006
|
function modelDisplay(modelV) {
|
|
24167
25007
|
if (isModelV(modelV)) {
|
|
24168
25008
|
const m = MODELS[modelV];
|
|
@@ -24171,7 +25011,7 @@ function modelDisplay(modelV) {
|
|
|
24171
25011
|
return { displayName: modelV, provider: "unknown" };
|
|
24172
25012
|
}
|
|
24173
25013
|
function usageRouter() {
|
|
24174
|
-
const r = new
|
|
25014
|
+
const r = new Hono12();
|
|
24175
25015
|
r.get("/summary", (c) => {
|
|
24176
25016
|
const s = getUsageSummary();
|
|
24177
25017
|
return c.json({
|
|
@@ -24220,8 +25060,293 @@ function usageRouter() {
|
|
|
24220
25060
|
return r;
|
|
24221
25061
|
}
|
|
24222
25062
|
|
|
25063
|
+
// src/routes/voice-credentials.ts
|
|
25064
|
+
import { Hono as Hono13 } from "hono";
|
|
25065
|
+
|
|
25066
|
+
// src/storage/reconcile-voices.ts
|
|
25067
|
+
var MINIMAX_SEED_VOICES = [
|
|
25068
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "male-qn-qingse" },
|
|
25069
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "female-shaonv" },
|
|
25070
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "female-yujie" },
|
|
25071
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "male-qn-jingying" },
|
|
25072
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "female-chengshu" },
|
|
25073
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "female-tianmei" }
|
|
25074
|
+
];
|
|
25075
|
+
var ELEVENLABS_SEED_VOICES = [
|
|
25076
|
+
{ provider: "elevenlabs", model: "eleven_multilingual_v2", voiceId: "21m00Tcm4TlvDq8ikWAM" },
|
|
25077
|
+
{ provider: "elevenlabs", model: "eleven_multilingual_v2", voiceId: "JBFqnCBsd6RMkjVDRZzb" }
|
|
25078
|
+
];
|
|
25079
|
+
function shuffle(arr) {
|
|
25080
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
25081
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
25082
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
25083
|
+
}
|
|
25084
|
+
return arr;
|
|
25085
|
+
}
|
|
25086
|
+
function snapshotPrior(agent, priorProvider, targetProvider) {
|
|
25087
|
+
if (!priorProvider) return;
|
|
25088
|
+
if (priorProvider === targetProvider) return;
|
|
25089
|
+
if (!agent.voice) return;
|
|
25090
|
+
if (agent.voice.provider !== priorProvider) return;
|
|
25091
|
+
try {
|
|
25092
|
+
writeVoiceBucketEntry(agent.id, priorProvider, agent.voice);
|
|
25093
|
+
} catch (e) {
|
|
25094
|
+
process.stderr.write(
|
|
25095
|
+
`[reconcile-voices] snapshot failed for ${agent.id}: ${e instanceof Error ? e.message : String(e)}
|
|
25096
|
+
`
|
|
25097
|
+
);
|
|
25098
|
+
}
|
|
25099
|
+
}
|
|
25100
|
+
function reconcileAgentVoices(opts) {
|
|
25101
|
+
const targetProvider = getActiveVoiceProvider();
|
|
25102
|
+
const priorProvider = opts.priorProvider ?? null;
|
|
25103
|
+
const agents = listAllAgents();
|
|
25104
|
+
if (agents.length === 0) {
|
|
25105
|
+
return { changed: 0, cleared: 0, reason: opts.reason, toProvider: targetProvider };
|
|
25106
|
+
}
|
|
25107
|
+
if (!targetProvider) {
|
|
25108
|
+
let cleared = 0;
|
|
25109
|
+
for (const a of agents) {
|
|
25110
|
+
snapshotPrior(a, priorProvider, null);
|
|
25111
|
+
if (a.voice) {
|
|
25112
|
+
try {
|
|
25113
|
+
updateAgent(a.id, { voice: null });
|
|
25114
|
+
cleared++;
|
|
25115
|
+
} catch (e) {
|
|
25116
|
+
process.stderr.write(
|
|
25117
|
+
`[reconcile-voices] clear failed for ${a.id}: ${e instanceof Error ? e.message : String(e)}
|
|
25118
|
+
`
|
|
25119
|
+
);
|
|
25120
|
+
}
|
|
25121
|
+
}
|
|
25122
|
+
}
|
|
25123
|
+
process.stderr.write(`[reconcile-voices] reason=${opts.reason} toProvider=null cleared=${cleared}
|
|
25124
|
+
`);
|
|
25125
|
+
return { changed: 0, cleared, reason: opts.reason, toProvider: null };
|
|
25126
|
+
}
|
|
25127
|
+
const pool = targetProvider === "minimax" ? MINIMAX_SEED_VOICES : targetProvider === "elevenlabs" ? ELEVENLABS_SEED_VOICES : [];
|
|
25128
|
+
if (pool.length === 0) {
|
|
25129
|
+
process.stderr.write(`[reconcile-voices] reason=${opts.reason} toProvider=${targetProvider} no-pool \xB7 skipped
|
|
25130
|
+
`);
|
|
25131
|
+
return { changed: 0, cleared: 0, reason: opts.reason, toProvider: targetProvider };
|
|
25132
|
+
}
|
|
25133
|
+
const shuffled = shuffle([...pool]);
|
|
25134
|
+
let changed = 0;
|
|
25135
|
+
const targetVp = targetProvider;
|
|
25136
|
+
for (let i = 0; i < agents.length; i++) {
|
|
25137
|
+
const a = agents[i];
|
|
25138
|
+
snapshotPrior(a, priorProvider, targetVp);
|
|
25139
|
+
const bucket = getVoiceBucket(a.id);
|
|
25140
|
+
const memorised = bucket[targetVp];
|
|
25141
|
+
const prev = a.voice;
|
|
25142
|
+
let profile;
|
|
25143
|
+
if (memorised) {
|
|
25144
|
+
profile = {
|
|
25145
|
+
provider: memorised.provider,
|
|
25146
|
+
model: memorised.model,
|
|
25147
|
+
voiceId: memorised.voiceId,
|
|
25148
|
+
...prev?.speed !== void 0 ? { speed: prev.speed } : memorised.speed !== void 0 ? { speed: memorised.speed } : {},
|
|
25149
|
+
...prev?.pitch !== void 0 ? { pitch: prev.pitch } : memorised.pitch !== void 0 ? { pitch: memorised.pitch } : {},
|
|
25150
|
+
...prev?.volume !== void 0 ? { volume: prev.volume } : memorised.volume !== void 0 ? { volume: memorised.volume } : {},
|
|
25151
|
+
...prev?.emotion !== void 0 ? { emotion: prev.emotion } : memorised.emotion !== void 0 ? { emotion: memorised.emotion } : {},
|
|
25152
|
+
...memorised.modifyPitch !== void 0 ? { modifyPitch: memorised.modifyPitch } : {},
|
|
25153
|
+
...memorised.modifyIntensity !== void 0 ? { modifyIntensity: memorised.modifyIntensity } : {},
|
|
25154
|
+
...memorised.modifyTimbre !== void 0 ? { modifyTimbre: memorised.modifyTimbre } : {},
|
|
25155
|
+
...memorised.instructions !== void 0 ? { instructions: memorised.instructions } : {}
|
|
25156
|
+
};
|
|
25157
|
+
} else {
|
|
25158
|
+
if (opts.reason === "first-key" && a.voice && a.voice.provider === targetVp) {
|
|
25159
|
+
if (!bucket[targetVp]) {
|
|
25160
|
+
try {
|
|
25161
|
+
writeVoiceBucketEntry(a.id, targetVp, a.voice);
|
|
25162
|
+
} catch (e) {
|
|
25163
|
+
process.stderr.write(
|
|
25164
|
+
`[reconcile-voices] seed failed for ${a.id}: ${e instanceof Error ? e.message : String(e)}
|
|
25165
|
+
`
|
|
25166
|
+
);
|
|
25167
|
+
}
|
|
25168
|
+
}
|
|
25169
|
+
continue;
|
|
25170
|
+
}
|
|
25171
|
+
const pick = shuffled[i % shuffled.length];
|
|
25172
|
+
profile = {
|
|
25173
|
+
provider: pick.provider,
|
|
25174
|
+
model: pick.model,
|
|
25175
|
+
voiceId: pick.voiceId,
|
|
25176
|
+
...prev?.speed !== void 0 ? { speed: prev.speed } : {},
|
|
25177
|
+
...prev?.pitch !== void 0 ? { pitch: prev.pitch } : {},
|
|
25178
|
+
...prev?.volume !== void 0 ? { volume: prev.volume } : {},
|
|
25179
|
+
...prev?.emotion !== void 0 ? { emotion: prev.emotion } : {}
|
|
25180
|
+
};
|
|
25181
|
+
}
|
|
25182
|
+
try {
|
|
25183
|
+
updateAgent(a.id, { voice: profile });
|
|
25184
|
+
writeVoiceBucketEntry(a.id, targetVp, profile);
|
|
25185
|
+
changed++;
|
|
25186
|
+
} catch (e) {
|
|
25187
|
+
process.stderr.write(
|
|
25188
|
+
`[reconcile-voices] update failed for ${a.id}: ${e instanceof Error ? e.message : String(e)}
|
|
25189
|
+
`
|
|
25190
|
+
);
|
|
25191
|
+
}
|
|
25192
|
+
}
|
|
25193
|
+
process.stderr.write(
|
|
25194
|
+
`[reconcile-voices] reason=${opts.reason} toProvider=${targetProvider} changed=${changed}
|
|
25195
|
+
`
|
|
25196
|
+
);
|
|
25197
|
+
return { changed, cleared: 0, reason: opts.reason, toProvider: targetProvider };
|
|
25198
|
+
}
|
|
25199
|
+
|
|
25200
|
+
// src/routes/voice-credentials.ts
|
|
25201
|
+
function payloadFor3(meta, activeId) {
|
|
25202
|
+
return {
|
|
25203
|
+
id: meta.id,
|
|
25204
|
+
provider: meta.provider,
|
|
25205
|
+
label: meta.label,
|
|
25206
|
+
preview: meta.preview,
|
|
25207
|
+
createdAt: meta.createdAt,
|
|
25208
|
+
updatedAt: meta.updatedAt,
|
|
25209
|
+
isActive: meta.id === activeId
|
|
25210
|
+
};
|
|
25211
|
+
}
|
|
25212
|
+
function pickNextActiveVoiceId(removedProvider) {
|
|
25213
|
+
const all = listVoiceCredentials();
|
|
25214
|
+
if (all.length === 0) return null;
|
|
25215
|
+
if (removedProvider) {
|
|
25216
|
+
const sameProvider = all.filter((c) => c.provider === removedProvider);
|
|
25217
|
+
if (sameProvider.length > 0) {
|
|
25218
|
+
sameProvider.sort((a, b) => a.createdAt - b.createdAt);
|
|
25219
|
+
return sameProvider[0].id;
|
|
25220
|
+
}
|
|
25221
|
+
}
|
|
25222
|
+
const sorted = all.slice().sort((a, b) => {
|
|
25223
|
+
const ai = VOICE_PROVIDER_PRIORITY.indexOf(a.provider);
|
|
25224
|
+
const bi = VOICE_PROVIDER_PRIORITY.indexOf(b.provider);
|
|
25225
|
+
if (ai !== bi) return ai - bi;
|
|
25226
|
+
return a.createdAt - b.createdAt;
|
|
25227
|
+
});
|
|
25228
|
+
return sorted[0]?.id ?? null;
|
|
25229
|
+
}
|
|
25230
|
+
function voiceCredentialsRouter() {
|
|
25231
|
+
const r = new Hono13();
|
|
25232
|
+
r.get("/", (c) => {
|
|
25233
|
+
const activeId = getPrefs().activeVoiceCredentialId;
|
|
25234
|
+
const items = listVoiceCredentials().map((m) => payloadFor3(m, activeId));
|
|
25235
|
+
return c.json({
|
|
25236
|
+
credentials: items,
|
|
25237
|
+
activeId
|
|
25238
|
+
});
|
|
25239
|
+
});
|
|
25240
|
+
r.put("/active", async (c) => {
|
|
25241
|
+
let body;
|
|
25242
|
+
try {
|
|
25243
|
+
body = await c.req.json();
|
|
25244
|
+
} catch {
|
|
25245
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
25246
|
+
}
|
|
25247
|
+
const rawId = body?.id;
|
|
25248
|
+
let nextId;
|
|
25249
|
+
if (rawId === null || rawId === void 0) {
|
|
25250
|
+
nextId = null;
|
|
25251
|
+
} else if (typeof rawId === "string") {
|
|
25252
|
+
nextId = rawId;
|
|
25253
|
+
} else {
|
|
25254
|
+
return c.json({ error: "id must be a string or null" }, 400);
|
|
25255
|
+
}
|
|
25256
|
+
const prefs = getPrefs();
|
|
25257
|
+
const priorActiveId = prefs.activeVoiceCredentialId;
|
|
25258
|
+
const priorProvider = priorActiveId ? getVoiceCredentialMeta(priorActiveId)?.provider ?? null : null;
|
|
25259
|
+
let nextProvider = null;
|
|
25260
|
+
if (nextId) {
|
|
25261
|
+
const meta = getVoiceCredentialMeta(nextId);
|
|
25262
|
+
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
25263
|
+
nextProvider = meta.provider;
|
|
25264
|
+
updatePrefs({ activeVoiceCredentialId: nextId });
|
|
25265
|
+
} else {
|
|
25266
|
+
updatePrefs({ activeVoiceCredentialId: null });
|
|
25267
|
+
}
|
|
25268
|
+
if (priorProvider !== nextProvider) {
|
|
25269
|
+
try {
|
|
25270
|
+
reconcileAgentVoices({ reason: "provider-switch", priorProvider });
|
|
25271
|
+
} catch (e) {
|
|
25272
|
+
process.stderr.write(
|
|
25273
|
+
`[voice-credentials.active] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
25274
|
+
`
|
|
25275
|
+
);
|
|
25276
|
+
}
|
|
25277
|
+
}
|
|
25278
|
+
return c.json({ activeId: nextId });
|
|
25279
|
+
});
|
|
25280
|
+
r.post("/", async (c) => {
|
|
25281
|
+
let body;
|
|
25282
|
+
try {
|
|
25283
|
+
body = await c.req.json();
|
|
25284
|
+
} catch {
|
|
25285
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
25286
|
+
}
|
|
25287
|
+
const provider = body?.provider;
|
|
25288
|
+
const labelRaw = body?.label;
|
|
25289
|
+
const key = body?.key;
|
|
25290
|
+
if (typeof provider !== "string" || !isVoiceProvider(provider)) {
|
|
25291
|
+
return c.json({ error: "provider must be 'minimax' or 'elevenlabs'" }, 400);
|
|
25292
|
+
}
|
|
25293
|
+
if (typeof key !== "string" || key.trim().length === 0) {
|
|
25294
|
+
return c.json({ error: "key must be a non-empty string" }, 400);
|
|
25295
|
+
}
|
|
25296
|
+
const label = typeof labelRaw === "string" ? labelRaw : null;
|
|
25297
|
+
const meta = createVoiceCredential(provider, label, key);
|
|
25298
|
+
if (!meta) return c.json({ error: "failed to create credential" }, 500);
|
|
25299
|
+
const hadActive = !!getPrefs().activeVoiceCredentialId;
|
|
25300
|
+
if (!hadActive) {
|
|
25301
|
+
updatePrefs({ activeVoiceCredentialId: meta.id });
|
|
25302
|
+
try {
|
|
25303
|
+
reconcileAgentVoices({ reason: "first-key", priorProvider: null });
|
|
25304
|
+
} catch (e) {
|
|
25305
|
+
process.stderr.write(
|
|
25306
|
+
`[voice-credentials.post] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
25307
|
+
`
|
|
25308
|
+
);
|
|
25309
|
+
}
|
|
25310
|
+
}
|
|
25311
|
+
const activeId = getPrefs().activeVoiceCredentialId;
|
|
25312
|
+
return c.json(payloadFor3(meta, activeId), 201);
|
|
25313
|
+
});
|
|
25314
|
+
r.delete("/:id", (c) => {
|
|
25315
|
+
const id = c.req.param("id");
|
|
25316
|
+
const meta = getVoiceCredentialMeta(id);
|
|
25317
|
+
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
25318
|
+
const prefs = getPrefs();
|
|
25319
|
+
const wasActive = prefs.activeVoiceCredentialId === id;
|
|
25320
|
+
const removedProvider = deleteVoiceCredential(id);
|
|
25321
|
+
let reshuffled = false;
|
|
25322
|
+
if (wasActive) {
|
|
25323
|
+
const nextId = pickNextActiveVoiceId(removedProvider);
|
|
25324
|
+
updatePrefs({ activeVoiceCredentialId: nextId });
|
|
25325
|
+
const nextProvider = nextId ? getVoiceCredentialMeta(nextId)?.provider ?? null : null;
|
|
25326
|
+
if (nextProvider !== removedProvider) {
|
|
25327
|
+
try {
|
|
25328
|
+
reconcileAgentVoices({ reason: "provider-switch", priorProvider: removedProvider });
|
|
25329
|
+
reshuffled = true;
|
|
25330
|
+
} catch (e) {
|
|
25331
|
+
process.stderr.write(
|
|
25332
|
+
`[voice-credentials.delete] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
25333
|
+
`
|
|
25334
|
+
);
|
|
25335
|
+
}
|
|
25336
|
+
}
|
|
25337
|
+
}
|
|
25338
|
+
return c.json({
|
|
25339
|
+
id,
|
|
25340
|
+
deleted: true,
|
|
25341
|
+
activeId: getPrefs().activeVoiceCredentialId,
|
|
25342
|
+
reshuffled
|
|
25343
|
+
});
|
|
25344
|
+
});
|
|
25345
|
+
return r;
|
|
25346
|
+
}
|
|
25347
|
+
|
|
24223
25348
|
// src/routes/voices.ts
|
|
24224
|
-
import { Hono as
|
|
25349
|
+
import { Hono as Hono14 } from "hono";
|
|
24225
25350
|
function ttsErrorMessage(e, providerLabel) {
|
|
24226
25351
|
if (!(e instanceof Error)) return String(e);
|
|
24227
25352
|
const cause = e.cause;
|
|
@@ -24266,8 +25391,15 @@ function ttsCacheSet(key, val) {
|
|
|
24266
25391
|
}
|
|
24267
25392
|
}
|
|
24268
25393
|
function voicesRouter() {
|
|
24269
|
-
const r = new
|
|
24270
|
-
r.get("/", async (c) =>
|
|
25394
|
+
const r = new Hono14();
|
|
25395
|
+
r.get("/", async (c) => {
|
|
25396
|
+
const catalog = await listAvailableVoices();
|
|
25397
|
+
return c.json({
|
|
25398
|
+
voices: catalog.voices,
|
|
25399
|
+
provider: catalog.provider,
|
|
25400
|
+
configured: catalog.configured
|
|
25401
|
+
});
|
|
25402
|
+
});
|
|
24271
25403
|
r.get("/message/:id/audio", (c) => {
|
|
24272
25404
|
const messageId = c.req.param("id");
|
|
24273
25405
|
const row = getUsableMessageVoice(messageId);
|
|
@@ -24407,7 +25539,7 @@ function voicesRouter() {
|
|
|
24407
25539
|
init_paths();
|
|
24408
25540
|
|
|
24409
25541
|
// src/version.ts
|
|
24410
|
-
var VERSION = "0.1.
|
|
25542
|
+
var VERSION = "0.1.32";
|
|
24411
25543
|
|
|
24412
25544
|
// src/utils/render-picker-catalog.ts
|
|
24413
25545
|
function renderPickerCatalog() {
|
|
@@ -24419,7 +25551,7 @@ function renderPickerCatalog() {
|
|
|
24419
25551
|
|
|
24420
25552
|
// src/server.ts
|
|
24421
25553
|
function createApp() {
|
|
24422
|
-
const app = new
|
|
25554
|
+
const app = new Hono15();
|
|
24423
25555
|
const dir = publicDir();
|
|
24424
25556
|
if (!existsSync2(dir)) {
|
|
24425
25557
|
throw new Error(
|
|
@@ -24467,7 +25599,9 @@ Build the package or check that public/ is bundled alongside dist/.`
|
|
|
24467
25599
|
app.route("/api/avatar", avatarRouter());
|
|
24468
25600
|
app.route("/api/usage", usageRouter());
|
|
24469
25601
|
app.route("/api/voices", voicesRouter());
|
|
25602
|
+
app.route("/api/voice-credentials", voiceCredentialsRouter());
|
|
24470
25603
|
app.route("/api/search", searchRouter());
|
|
25604
|
+
app.route("/api/search-credentials", searchCredentialsRouter());
|
|
24471
25605
|
app.use(
|
|
24472
25606
|
"/*",
|
|
24473
25607
|
serveStatic({
|
|
@@ -24488,6 +25622,10 @@ async function startServer(opts) {
|
|
|
24488
25622
|
url: `http://${host}:${opts.port}`,
|
|
24489
25623
|
close: () => new Promise((resolve2, reject) => {
|
|
24490
25624
|
server.close((err2) => err2 ? reject(err2) : resolve2());
|
|
25625
|
+
const maybeForceClose = server.closeAllConnections;
|
|
25626
|
+
if (typeof maybeForceClose === "function") {
|
|
25627
|
+
maybeForceClose.call(server);
|
|
25628
|
+
}
|
|
24491
25629
|
})
|
|
24492
25630
|
};
|
|
24493
25631
|
}
|