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/cli.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
|
}
|
|
@@ -1181,7 +1221,9 @@ var VALID_CARRIER_PREFS = /* @__PURE__ */ new Set([
|
|
|
1181
1221
|
"anthropic",
|
|
1182
1222
|
"openai",
|
|
1183
1223
|
"google",
|
|
1184
|
-
"xai"
|
|
1224
|
+
"xai",
|
|
1225
|
+
"moonshot",
|
|
1226
|
+
"zhipu"
|
|
1185
1227
|
]);
|
|
1186
1228
|
function parseCarrierPref(raw) {
|
|
1187
1229
|
if (!raw) return null;
|
|
@@ -1404,6 +1446,54 @@ function parseBuildLog(raw) {
|
|
|
1404
1446
|
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
1405
1447
|
};
|
|
1406
1448
|
}
|
|
1449
|
+
function parseModelByProvider(raw) {
|
|
1450
|
+
if (!raw) return {};
|
|
1451
|
+
try {
|
|
1452
|
+
const obj = JSON.parse(raw);
|
|
1453
|
+
if (!obj || typeof obj !== "object") return {};
|
|
1454
|
+
const out = {};
|
|
1455
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1456
|
+
if (VALID_CARRIER_PREFS.has(k) && typeof v === "string" && v.trim().length > 0) {
|
|
1457
|
+
out[k] = v.trim();
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return out;
|
|
1461
|
+
} catch {
|
|
1462
|
+
return {};
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function parseVoiceByProvider(raw) {
|
|
1466
|
+
if (!raw) return {};
|
|
1467
|
+
try {
|
|
1468
|
+
const obj = JSON.parse(raw);
|
|
1469
|
+
if (!obj || typeof obj !== "object") return {};
|
|
1470
|
+
const out = {};
|
|
1471
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1472
|
+
if (k !== "minimax" && k !== "elevenlabs") continue;
|
|
1473
|
+
const profile = parseVoice(JSON.stringify(v));
|
|
1474
|
+
if (profile) out[k] = profile;
|
|
1475
|
+
}
|
|
1476
|
+
return out;
|
|
1477
|
+
} catch {
|
|
1478
|
+
return {};
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
function serializeModelByProvider(b) {
|
|
1482
|
+
if (!b || Object.keys(b).length === 0) return null;
|
|
1483
|
+
return JSON.stringify(b);
|
|
1484
|
+
}
|
|
1485
|
+
function serializeVoiceByProvider(b) {
|
|
1486
|
+
if (!b || Object.keys(b).length === 0) return null;
|
|
1487
|
+
const out = {};
|
|
1488
|
+
for (const [k, v] of Object.entries(b)) {
|
|
1489
|
+
if (!v) continue;
|
|
1490
|
+
const json = serializeVoice(v);
|
|
1491
|
+
if (!json) continue;
|
|
1492
|
+
out[k] = JSON.parse(json);
|
|
1493
|
+
}
|
|
1494
|
+
if (Object.keys(out).length === 0) return null;
|
|
1495
|
+
return JSON.stringify(out);
|
|
1496
|
+
}
|
|
1407
1497
|
function serializeVoice(v) {
|
|
1408
1498
|
if (!v) return null;
|
|
1409
1499
|
const provider = VALID_VOICE_PROVIDERS.has(v.provider) ? v.provider : null;
|
|
@@ -1451,7 +1541,7 @@ function mapRow(row) {
|
|
|
1451
1541
|
updatedAt: row.updated_at
|
|
1452
1542
|
};
|
|
1453
1543
|
}
|
|
1454
|
-
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";
|
|
1544
|
+
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";
|
|
1455
1545
|
function listAgents() {
|
|
1456
1546
|
const rows = getDb().prepare(
|
|
1457
1547
|
`SELECT ${SELECT_COLS} FROM agents
|
|
@@ -1760,6 +1850,47 @@ function updateAgent(id, patch) {
|
|
|
1760
1850
|
if (r.changes === 0) return null;
|
|
1761
1851
|
return getAgent(id);
|
|
1762
1852
|
}
|
|
1853
|
+
function getModelBucket(id) {
|
|
1854
|
+
const row = getDb().prepare("SELECT model_by_provider_json FROM agents WHERE id = ?").get(id);
|
|
1855
|
+
if (!row) return {};
|
|
1856
|
+
return parseModelByProvider(row.model_by_provider_json);
|
|
1857
|
+
}
|
|
1858
|
+
function getVoiceBucket(id) {
|
|
1859
|
+
const row = getDb().prepare("SELECT voice_by_provider_json FROM agents WHERE id = ?").get(id);
|
|
1860
|
+
if (!row) return {};
|
|
1861
|
+
return parseVoiceByProvider(row.voice_by_provider_json);
|
|
1862
|
+
}
|
|
1863
|
+
function writeModelBucketEntry(id, carrier, modelV) {
|
|
1864
|
+
if (!modelV) return;
|
|
1865
|
+
const bucket = getModelBucket(id);
|
|
1866
|
+
if (bucket[carrier] === modelV) return;
|
|
1867
|
+
bucket[carrier] = modelV;
|
|
1868
|
+
const serialized = serializeModelByProvider(bucket);
|
|
1869
|
+
getDb().prepare("UPDATE agents SET model_by_provider_json = ?, updated_at = ? WHERE id = ?").run(serialized, Date.now(), id);
|
|
1870
|
+
}
|
|
1871
|
+
function writeVoiceBucketEntry(id, provider, voice) {
|
|
1872
|
+
const normalized = parseVoice(serializeVoice(voice));
|
|
1873
|
+
if (!normalized || normalized.provider !== "minimax" && normalized.provider !== "elevenlabs") return;
|
|
1874
|
+
if (normalized.provider !== provider) return;
|
|
1875
|
+
const bucket = getVoiceBucket(id);
|
|
1876
|
+
bucket[provider] = normalized;
|
|
1877
|
+
const serialized = serializeVoiceByProvider(bucket);
|
|
1878
|
+
getDb().prepare("UPDATE agents SET voice_by_provider_json = ?, updated_at = ? WHERE id = ?").run(serialized, Date.now(), id);
|
|
1879
|
+
}
|
|
1880
|
+
function deleteModelBucketEntry(id, carrier) {
|
|
1881
|
+
const bucket = getModelBucket(id);
|
|
1882
|
+
if (!(carrier in bucket)) return;
|
|
1883
|
+
delete bucket[carrier];
|
|
1884
|
+
const serialized = serializeModelByProvider(bucket);
|
|
1885
|
+
getDb().prepare("UPDATE agents SET model_by_provider_json = ?, updated_at = ? WHERE id = ?").run(serialized, Date.now(), id);
|
|
1886
|
+
}
|
|
1887
|
+
function deleteVoiceBucketEntry(id, provider) {
|
|
1888
|
+
const bucket = getVoiceBucket(id);
|
|
1889
|
+
if (!(provider in bucket)) return;
|
|
1890
|
+
delete bucket[provider];
|
|
1891
|
+
const serialized = serializeVoiceByProvider(bucket);
|
|
1892
|
+
getDb().prepare("UPDATE agents SET voice_by_provider_json = ?, updated_at = ? WHERE id = ?").run(serialized, Date.now(), id);
|
|
1893
|
+
}
|
|
1763
1894
|
|
|
1764
1895
|
// src/seed/run.ts
|
|
1765
1896
|
init_db();
|
|
@@ -2478,7 +2609,7 @@ function runSeed() {
|
|
|
2478
2609
|
// src/server.ts
|
|
2479
2610
|
import { serve } from "@hono/node-server";
|
|
2480
2611
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
2481
|
-
import { Hono as
|
|
2612
|
+
import { Hono as Hono15 } from "hono";
|
|
2482
2613
|
import { existsSync as existsSync2 } from "fs";
|
|
2483
2614
|
|
|
2484
2615
|
// src/routes/agents.ts
|
|
@@ -2653,10 +2784,11 @@ var MODELS = {
|
|
|
2653
2784
|
deck: "V4 Flash \xB7 fast \xB7 1M ctx",
|
|
2654
2785
|
viaUniversalOnly: true
|
|
2655
2786
|
},
|
|
2656
|
-
// ── Zhipu (Z.AI) · GLM family · OR + B.AI
|
|
2657
|
-
//
|
|
2658
|
-
//
|
|
2659
|
-
//
|
|
2787
|
+
// ── Zhipu (Z.AI) · GLM family · direct + OR + B.AI ──
|
|
2788
|
+
// Direct route uses Zhipu's OpenAI-compatible chat-completions API
|
|
2789
|
+
// at https://open.bigmodel.cn/api/paas/v4/ (see adapter.ts
|
|
2790
|
+
// case "zhipu"). OpenRouter catalog convention: `z-ai/glm-X.Y`.
|
|
2791
|
+
// B.AI uses dotted lowercase: `glm-5.1`.
|
|
2660
2792
|
"glm-5-1": {
|
|
2661
2793
|
v: "glm-5-1",
|
|
2662
2794
|
provider: "zhipu",
|
|
@@ -2665,16 +2797,16 @@ var MODELS = {
|
|
|
2665
2797
|
baiId: "glm-5.1",
|
|
2666
2798
|
displayName: "GLM 5.1",
|
|
2667
2799
|
contextBudget: 2e5,
|
|
2668
|
-
deck: "Zhipu flagship \xB7 200k ctx"
|
|
2669
|
-
viaUniversalOnly: true
|
|
2800
|
+
deck: "Zhipu flagship \xB7 200k ctx"
|
|
2670
2801
|
},
|
|
2671
|
-
// ── Moonshot · Kimi family · OR + B.AI ──
|
|
2802
|
+
// ── Moonshot · Kimi family · direct + OR + B.AI ──
|
|
2803
|
+
// Direct route uses Moonshot's OpenAI-compatible chat-completions
|
|
2804
|
+
// API at https://api.moonshot.cn/v1 (see adapter.ts case "moonshot").
|
|
2672
2805
|
// OpenRouter catalog convention: `moonshotai/kimi-k2.6` (the leading
|
|
2673
2806
|
// `k` is part of the slug — `moonshotai/kimi-2.6` 404s). B.AI's
|
|
2674
2807
|
// siliconflow distributor still ships the older `kimi-k2.5` channel
|
|
2675
2808
|
// (per 2026-05-17 catalog snapshot), so the B.AI route serves K2.5
|
|
2676
|
-
// until B.AI picks up the newer build.
|
|
2677
|
-
// viaUniversalOnly skips the direct path.
|
|
2809
|
+
// until B.AI picks up the newer build.
|
|
2678
2810
|
"kimi-k2-6": {
|
|
2679
2811
|
v: "kimi-k2-6",
|
|
2680
2812
|
provider: "moonshot",
|
|
@@ -2683,8 +2815,7 @@ var MODELS = {
|
|
|
2683
2815
|
baiId: "kimi-k2.5",
|
|
2684
2816
|
displayName: "Kimi K2.6",
|
|
2685
2817
|
contextBudget: 256e3,
|
|
2686
|
-
deck: "Moonshot \xB7 long-context"
|
|
2687
|
-
viaUniversalOnly: true
|
|
2818
|
+
deck: "Moonshot \xB7 long-context"
|
|
2688
2819
|
},
|
|
2689
2820
|
// ── MiniMax · M-series · OR + B.AI ──
|
|
2690
2821
|
// No direct @ai-sdk client · viaUniversalOnly skips the direct path.
|
|
@@ -2781,7 +2912,11 @@ var PROFILE_SYSTEM = [
|
|
|
2781
2912
|
"Constraints:",
|
|
2782
2913
|
"\xB7 DO NOT use generic personality words. Every entry names a person / case / concept / position.",
|
|
2783
2914
|
"\xB7 If the user description maps to a real domain (VC, product, security, biotech, monetary policy, etc.), prefer NAMED references from that domain.",
|
|
2784
|
-
"\xB7 Avoid recreating the canonical six (Socrates, First Principles, Long Horizon, etc.) \u2014 pick a distinct angle."
|
|
2915
|
+
"\xB7 Avoid recreating the canonical six (Socrates, First Principles, Long Horizon, etc.) \u2014 pick a distinct angle.",
|
|
2916
|
+
"",
|
|
2917
|
+
"## CRITICAL \xB7 output format",
|
|
2918
|
+
"",
|
|
2919
|
+
"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."
|
|
2785
2920
|
].join("\n");
|
|
2786
2921
|
var HOUSE_STYLE = [
|
|
2787
2922
|
"## Boardroom directors \xB7 house style",
|
|
@@ -2902,7 +3037,7 @@ function buildAgentProfileMessages(opts) {
|
|
|
2902
3037
|
}
|
|
2903
3038
|
userBody.push(
|
|
2904
3039
|
"",
|
|
2905
|
-
"Now produce the profile JSON object as specified."
|
|
3040
|
+
"Now produce the profile JSON object as specified \u2014 ```json fenced block, exact camelCase field names, no prose before or after."
|
|
2906
3041
|
);
|
|
2907
3042
|
return [
|
|
2908
3043
|
{ role: "system", content: PROFILE_SYSTEM },
|
|
@@ -3043,29 +3178,98 @@ function parseAgentSpec(raw) {
|
|
|
3043
3178
|
ability
|
|
3044
3179
|
};
|
|
3045
3180
|
}
|
|
3181
|
+
function pickArrayField(parsed, aliases) {
|
|
3182
|
+
for (const k of aliases) {
|
|
3183
|
+
const v = parsed[k];
|
|
3184
|
+
if (Array.isArray(v)) return v;
|
|
3185
|
+
}
|
|
3186
|
+
return [];
|
|
3187
|
+
}
|
|
3188
|
+
function pickObjectField(parsed, aliases) {
|
|
3189
|
+
for (const k of aliases) {
|
|
3190
|
+
const v = parsed[k];
|
|
3191
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
3192
|
+
}
|
|
3193
|
+
return {};
|
|
3194
|
+
}
|
|
3195
|
+
function coerceConceptEntry(c) {
|
|
3196
|
+
if (typeof c === "string") {
|
|
3197
|
+
const s = c.trim();
|
|
3198
|
+
const m = /^(.+?)\s*[·:\-—]\s*(.+)$/.exec(s);
|
|
3199
|
+
if (m) return { name: clamp(m[1].trim(), 80), gloss: clamp(m[2].trim(), 200) };
|
|
3200
|
+
return { name: clamp(s, 80), gloss: "" };
|
|
3201
|
+
}
|
|
3202
|
+
if (!c || typeof c !== "object") return { name: "", gloss: "" };
|
|
3203
|
+
const o = c;
|
|
3204
|
+
const name = [o.name, o.concept, o.title, o.handle, o.term].find(
|
|
3205
|
+
(x) => typeof x === "string" && x.trim().length > 0
|
|
3206
|
+
);
|
|
3207
|
+
const gloss = [o.gloss, o.description, o.desc, o.detail, o.explanation, o.summary].find(
|
|
3208
|
+
(x) => typeof x === "string" && x.trim().length > 0
|
|
3209
|
+
);
|
|
3210
|
+
return {
|
|
3211
|
+
name: name ? clamp(name.trim(), 80) : "",
|
|
3212
|
+
gloss: gloss ? clamp(gloss.trim(), 200) : ""
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3215
|
+
function coerceReferentEntry(r) {
|
|
3216
|
+
if (typeof r === "string") {
|
|
3217
|
+
const s = r.trim();
|
|
3218
|
+
const m = /^(.+?)\s*[·:\-—]\s*(.+)$/.exec(s);
|
|
3219
|
+
if (m) return { ref: clamp(m[1].trim(), 80), why: clamp(m[2].trim(), 200) };
|
|
3220
|
+
return { ref: clamp(s, 80), why: "" };
|
|
3221
|
+
}
|
|
3222
|
+
if (!r || typeof r !== "object") return { ref: "", why: "" };
|
|
3223
|
+
const o = r;
|
|
3224
|
+
const ref = [o.ref, o.name, o.reference, o.anchor, o.title, o.case].find(
|
|
3225
|
+
(x) => typeof x === "string" && x.trim().length > 0
|
|
3226
|
+
);
|
|
3227
|
+
const why = [o.why, o.reason, o.relevance, o.gloss, o.description, o.note].find(
|
|
3228
|
+
(x) => typeof x === "string" && x.trim().length > 0
|
|
3229
|
+
);
|
|
3230
|
+
return {
|
|
3231
|
+
ref: ref ? clamp(ref.trim(), 80) : "",
|
|
3232
|
+
why: why ? clamp(why.trim(), 200) : ""
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3046
3235
|
function parseAgentProfile(raw) {
|
|
3047
3236
|
const parsed = extractJson(raw);
|
|
3048
3237
|
if (!parsed) return null;
|
|
3049
|
-
const lineage = parsed
|
|
3050
|
-
const influencedByRaw =
|
|
3051
|
-
const opposedToRaw =
|
|
3238
|
+
const lineage = pickObjectField(parsed, ["intellectualLineage", "intellectual_lineage", "lineage"]);
|
|
3239
|
+
const influencedByRaw = pickArrayField(lineage, ["influencedBy", "influenced_by", "influences", "influenced"]);
|
|
3240
|
+
const opposedToRaw = pickArrayField(lineage, ["opposedTo", "opposed_to", "opposes", "against"]);
|
|
3052
3241
|
const influencedBy = influencedByRaw.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => clamp(s.trim(), 200)).slice(0, 5);
|
|
3053
3242
|
const opposedTo = opposedToRaw.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => clamp(s.trim(), 200)).slice(0, 4);
|
|
3054
|
-
const conceptsRaw =
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3243
|
+
const conceptsRaw = pickArrayField(parsed, [
|
|
3244
|
+
"loadBearingConcepts",
|
|
3245
|
+
"load_bearing_concepts",
|
|
3246
|
+
"concepts",
|
|
3247
|
+
"frames",
|
|
3248
|
+
"mentalTools",
|
|
3249
|
+
"mental_tools"
|
|
3250
|
+
]);
|
|
3251
|
+
const loadBearingConcepts = conceptsRaw.map(coerceConceptEntry).filter((c) => c.name.length > 0).slice(0, 6);
|
|
3252
|
+
const referentsRaw = pickArrayField(parsed, [
|
|
3253
|
+
"referentSet",
|
|
3254
|
+
"referent_set",
|
|
3255
|
+
"referents",
|
|
3256
|
+
"anchors",
|
|
3257
|
+
"references",
|
|
3258
|
+
"citations"
|
|
3259
|
+
]);
|
|
3260
|
+
const referentSet = referentsRaw.map(coerceReferentEntry).filter((r) => r.ref.length > 0).slice(0, 6);
|
|
3261
|
+
const failureModesRaw = pickArrayField(parsed, ["failureModes", "failure_modes", "blindSpots", "blind_spots"]);
|
|
3065
3262
|
const failureModes = failureModesRaw.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => clamp(s.trim(), 220)).slice(0, 4);
|
|
3066
|
-
const contrarianTakesRaw =
|
|
3263
|
+
const contrarianTakesRaw = pickArrayField(parsed, [
|
|
3264
|
+
"contrarianTakes",
|
|
3265
|
+
"contrarian_takes",
|
|
3266
|
+
"contrarianViews",
|
|
3267
|
+
"contrarian_views",
|
|
3268
|
+
"takes"
|
|
3269
|
+
]);
|
|
3067
3270
|
const contrarianTakes = contrarianTakesRaw.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => clamp(s.trim(), 220)).slice(0, 4);
|
|
3068
|
-
|
|
3271
|
+
const anyPopulated = loadBearingConcepts.length > 0 || referentSet.length > 0 || influencedBy.length > 0 || opposedTo.length > 0 || failureModes.length > 0 || contrarianTakes.length > 0;
|
|
3272
|
+
if (!anyPopulated) return null;
|
|
3069
3273
|
return {
|
|
3070
3274
|
intellectualLineage: { influencedBy, opposedTo },
|
|
3071
3275
|
loadBearingConcepts,
|
|
@@ -3555,8 +3759,7 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
|
3555
3759
|
import { APICallError, streamText } from "ai";
|
|
3556
3760
|
|
|
3557
3761
|
// src/storage/credentials.ts
|
|
3558
|
-
import {
|
|
3559
|
-
import { userInfo } from "os";
|
|
3762
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
3560
3763
|
|
|
3561
3764
|
// src/ai/providers.ts
|
|
3562
3765
|
var MULTI_MODEL_LLM_PROVIDERS = [
|
|
@@ -3567,7 +3770,9 @@ var SINGLE_MODEL_LLM_PROVIDERS = [
|
|
|
3567
3770
|
"anthropic",
|
|
3568
3771
|
"openai",
|
|
3569
3772
|
"google",
|
|
3570
|
-
"xai"
|
|
3773
|
+
"xai",
|
|
3774
|
+
"moonshot",
|
|
3775
|
+
"zhipu"
|
|
3571
3776
|
];
|
|
3572
3777
|
var ALL_LLM_PROVIDERS = [
|
|
3573
3778
|
...MULTI_MODEL_LLM_PROVIDERS,
|
|
@@ -3579,7 +3784,9 @@ var LLM_PROVIDER_PRIORITY = [
|
|
|
3579
3784
|
"anthropic",
|
|
3580
3785
|
"openai",
|
|
3581
3786
|
"google",
|
|
3582
|
-
"xai"
|
|
3787
|
+
"xai",
|
|
3788
|
+
"moonshot",
|
|
3789
|
+
"zhipu"
|
|
3583
3790
|
];
|
|
3584
3791
|
function isMultiModelProvider(p) {
|
|
3585
3792
|
return p === "openrouter" || p === "bai";
|
|
@@ -3590,6 +3797,10 @@ function isLlmProvider(p) {
|
|
|
3590
3797
|
|
|
3591
3798
|
// src/storage/credentials.ts
|
|
3592
3799
|
init_db();
|
|
3800
|
+
|
|
3801
|
+
// src/storage/credential-crypto.ts
|
|
3802
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
|
|
3803
|
+
import { userInfo } from "os";
|
|
3593
3804
|
var SALT = "boardroom.v1.salt";
|
|
3594
3805
|
var ALGO = "aes-256-gcm";
|
|
3595
3806
|
var _key = null;
|
|
@@ -3599,14 +3810,14 @@ function deriveKey() {
|
|
|
3599
3810
|
_key = scryptSync(username, SALT, 32);
|
|
3600
3811
|
return _key;
|
|
3601
3812
|
}
|
|
3602
|
-
function
|
|
3813
|
+
function encryptCredential(plain) {
|
|
3603
3814
|
const iv = randomBytes(12);
|
|
3604
3815
|
const cipher = createCipheriv(ALGO, deriveKey(), iv);
|
|
3605
3816
|
const ct = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
3606
3817
|
const tag = cipher.getAuthTag();
|
|
3607
3818
|
return Buffer.concat([iv, tag, ct]);
|
|
3608
3819
|
}
|
|
3609
|
-
function
|
|
3820
|
+
function decryptCredential(blob) {
|
|
3610
3821
|
const iv = blob.subarray(0, 12);
|
|
3611
3822
|
const tag = blob.subarray(12, 28);
|
|
3612
3823
|
const ct = blob.subarray(28);
|
|
@@ -3614,7 +3825,7 @@ function decrypt(blob) {
|
|
|
3614
3825
|
decipher.setAuthTag(tag);
|
|
3615
3826
|
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
|
|
3616
3827
|
}
|
|
3617
|
-
function
|
|
3828
|
+
function maskCredential(plain) {
|
|
3618
3829
|
const trimmed = plain.trim();
|
|
3619
3830
|
if (!trimmed) return "";
|
|
3620
3831
|
const n = trimmed.length;
|
|
@@ -3622,11 +3833,13 @@ function maskKey(plain) {
|
|
|
3622
3833
|
if (n <= 12) return `${trimmed.slice(0, 2)}${"\u2022".repeat(n - 4)}${trimmed.slice(-2)}`;
|
|
3623
3834
|
return `${trimmed.slice(0, 4)}${"\u2022".repeat(n - 8)}${trimmed.slice(-4)}`;
|
|
3624
3835
|
}
|
|
3836
|
+
|
|
3837
|
+
// src/storage/credentials.ts
|
|
3625
3838
|
function rowToMeta(row) {
|
|
3626
3839
|
if (!isLlmProvider(row.provider)) return null;
|
|
3627
3840
|
let preview = "";
|
|
3628
3841
|
try {
|
|
3629
|
-
preview =
|
|
3842
|
+
preview = maskCredential(decryptCredential(row.key_blob));
|
|
3630
3843
|
} catch {
|
|
3631
3844
|
preview = "";
|
|
3632
3845
|
}
|
|
@@ -3652,7 +3865,7 @@ function getLlmCredentialKey(id) {
|
|
|
3652
3865
|
const row = getDb().prepare("SELECT key_blob FROM llm_credentials WHERE id = ?").get(id);
|
|
3653
3866
|
if (!row) return null;
|
|
3654
3867
|
try {
|
|
3655
|
-
return
|
|
3868
|
+
return decryptCredential(row.key_blob);
|
|
3656
3869
|
} catch {
|
|
3657
3870
|
return null;
|
|
3658
3871
|
}
|
|
@@ -3683,6 +3896,15 @@ function providerDisplayName(provider) {
|
|
|
3683
3896
|
return "Gemini";
|
|
3684
3897
|
case "xai":
|
|
3685
3898
|
return "Grok";
|
|
3899
|
+
// Brand-name labels for the two newest direct providers · "Kimi"
|
|
3900
|
+
// (model brand) over "Moonshot" (company), "GLM" (model family)
|
|
3901
|
+
// over "Zhipu" (company) · same convention as "Claude"/anthropic
|
|
3902
|
+
// and "ChatGPT"/openai · the user-facing UI prefers the product
|
|
3903
|
+
// name over the corporate slug.
|
|
3904
|
+
case "moonshot":
|
|
3905
|
+
return "Kimi";
|
|
3906
|
+
case "zhipu":
|
|
3907
|
+
return "GLM";
|
|
3686
3908
|
}
|
|
3687
3909
|
}
|
|
3688
3910
|
function createLlmCredential(provider, label, plain) {
|
|
@@ -3690,8 +3912,8 @@ function createLlmCredential(provider, label, plain) {
|
|
|
3690
3912
|
if (!trimmed) return null;
|
|
3691
3913
|
if (!ALL_LLM_PROVIDERS.includes(provider)) return null;
|
|
3692
3914
|
const resolvedLabel = resolveFreeLabel(provider, label);
|
|
3693
|
-
const id =
|
|
3694
|
-
const blob =
|
|
3915
|
+
const id = randomBytes2(8).toString("hex");
|
|
3916
|
+
const blob = encryptCredential(trimmed);
|
|
3695
3917
|
const now = Date.now();
|
|
3696
3918
|
getDb().prepare(
|
|
3697
3919
|
`INSERT INTO llm_credentials (id, provider, label, key_blob, created_at, updated_at)
|
|
@@ -3725,6 +3947,8 @@ function mapRow3(row) {
|
|
|
3725
3947
|
minimaxRegion: normalizeMinimaxRegion(row.minimax_region),
|
|
3726
3948
|
activeLlmProvider: raw && isLlmProvider(raw) ? raw : null,
|
|
3727
3949
|
activeLlmCredentialId: row.active_llm_credential_id,
|
|
3950
|
+
activeVoiceCredentialId: row.active_voice_credential_id,
|
|
3951
|
+
activeSearchCredentialId: row.active_search_credential_id,
|
|
3728
3952
|
createdAt: row.created_at,
|
|
3729
3953
|
updatedAt: row.updated_at
|
|
3730
3954
|
};
|
|
@@ -3736,6 +3960,8 @@ function getPrefs() {
|
|
|
3736
3960
|
COALESCE(minimax_region, 'cn') AS minimax_region,
|
|
3737
3961
|
active_llm_provider,
|
|
3738
3962
|
active_llm_credential_id,
|
|
3963
|
+
active_voice_credential_id,
|
|
3964
|
+
active_search_credential_id,
|
|
3739
3965
|
created_at, updated_at FROM prefs WHERE id = 1`
|
|
3740
3966
|
).get();
|
|
3741
3967
|
if (!row) {
|
|
@@ -3778,6 +4004,14 @@ function updatePrefs(patch) {
|
|
|
3778
4004
|
fields.push("active_llm_credential_id = ?");
|
|
3779
4005
|
values.push(patch.activeLlmCredentialId);
|
|
3780
4006
|
}
|
|
4007
|
+
if (patch.activeVoiceCredentialId !== void 0) {
|
|
4008
|
+
fields.push("active_voice_credential_id = ?");
|
|
4009
|
+
values.push(patch.activeVoiceCredentialId);
|
|
4010
|
+
}
|
|
4011
|
+
if (patch.activeSearchCredentialId !== void 0) {
|
|
4012
|
+
fields.push("active_search_credential_id = ?");
|
|
4013
|
+
values.push(patch.activeSearchCredentialId);
|
|
4014
|
+
}
|
|
3781
4015
|
if (fields.length === 0) return getPrefs();
|
|
3782
4016
|
fields.push("updated_at = ?");
|
|
3783
4017
|
values.push(Date.now());
|
|
@@ -3987,6 +4221,30 @@ function directResolved(meta, apiKey) {
|
|
|
3987
4221
|
carrier: "xai"
|
|
3988
4222
|
};
|
|
3989
4223
|
}
|
|
4224
|
+
case "moonshot": {
|
|
4225
|
+
const compat = createOpenAICompatible({
|
|
4226
|
+
name: "moonshot",
|
|
4227
|
+
apiKey,
|
|
4228
|
+
baseURL: "https://api.moonshot.cn/v1",
|
|
4229
|
+
fetch: makeLoggedFetch("moonshot")
|
|
4230
|
+
});
|
|
4231
|
+
return {
|
|
4232
|
+
model: compat.chatModel(meta.directApiId),
|
|
4233
|
+
carrier: "moonshot"
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
case "zhipu": {
|
|
4237
|
+
const compat = createOpenAICompatible({
|
|
4238
|
+
name: "zhipu",
|
|
4239
|
+
apiKey,
|
|
4240
|
+
baseURL: "https://open.bigmodel.cn/api/paas/v4/",
|
|
4241
|
+
fetch: makeLoggedFetch("zhipu")
|
|
4242
|
+
});
|
|
4243
|
+
return {
|
|
4244
|
+
model: compat.chatModel(meta.directApiId),
|
|
4245
|
+
carrier: "zhipu"
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
3990
4248
|
default:
|
|
3991
4249
|
throw new NoKeyError(meta.provider);
|
|
3992
4250
|
}
|
|
@@ -4189,6 +4447,7 @@ async function callLLM(req) {
|
|
|
4189
4447
|
async function callLLMWithUsage(req) {
|
|
4190
4448
|
let buf = "";
|
|
4191
4449
|
let usage = null;
|
|
4450
|
+
let finishReason = null;
|
|
4192
4451
|
for await (const chunk of callLLMStream(req)) {
|
|
4193
4452
|
if (chunk.type === "text") buf += chunk.delta;
|
|
4194
4453
|
else if (chunk.type === "error") throw new Error(chunk.message);
|
|
@@ -4198,15 +4457,118 @@ async function callLLMWithUsage(req) {
|
|
|
4198
4457
|
completionTokens: chunk.completionTokens,
|
|
4199
4458
|
totalTokens: chunk.totalTokens
|
|
4200
4459
|
};
|
|
4460
|
+
} else if (chunk.type === "done") {
|
|
4461
|
+
if (typeof chunk.finishReason === "string" && chunk.finishReason.length > 0) {
|
|
4462
|
+
finishReason = chunk.finishReason;
|
|
4463
|
+
}
|
|
4201
4464
|
}
|
|
4202
4465
|
}
|
|
4203
|
-
return { text: buf, usage };
|
|
4466
|
+
return { text: buf, usage, finishReason };
|
|
4204
4467
|
}
|
|
4205
4468
|
|
|
4206
4469
|
// src/storage/keys.ts
|
|
4207
4470
|
init_db();
|
|
4208
|
-
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as
|
|
4471
|
+
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes4, scryptSync as scryptSync2 } from "crypto";
|
|
4209
4472
|
import { userInfo as userInfo2 } from "os";
|
|
4473
|
+
|
|
4474
|
+
// src/storage/search-credentials.ts
|
|
4475
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
4476
|
+
init_db();
|
|
4477
|
+
var ALL_SEARCH_PROVIDERS = ["brave", "tavily"];
|
|
4478
|
+
function isSearchProvider(p) {
|
|
4479
|
+
return ALL_SEARCH_PROVIDERS.includes(p);
|
|
4480
|
+
}
|
|
4481
|
+
var SEARCH_PROVIDER_PRIORITY = ["brave", "tavily"];
|
|
4482
|
+
function rowToMeta2(row) {
|
|
4483
|
+
if (!isSearchProvider(row.provider)) return null;
|
|
4484
|
+
let preview = "";
|
|
4485
|
+
try {
|
|
4486
|
+
preview = maskCredential(decryptCredential(row.key_blob));
|
|
4487
|
+
} catch {
|
|
4488
|
+
preview = "";
|
|
4489
|
+
}
|
|
4490
|
+
return {
|
|
4491
|
+
id: row.id,
|
|
4492
|
+
provider: row.provider,
|
|
4493
|
+
label: row.label,
|
|
4494
|
+
preview,
|
|
4495
|
+
createdAt: row.created_at,
|
|
4496
|
+
updatedAt: row.updated_at
|
|
4497
|
+
};
|
|
4498
|
+
}
|
|
4499
|
+
function listSearchCredentials() {
|
|
4500
|
+
const rows = getDb().prepare("SELECT id, provider, label, key_blob, created_at, updated_at FROM search_credentials ORDER BY created_at ASC").all();
|
|
4501
|
+
return rows.map(rowToMeta2).filter((m) => m !== null);
|
|
4502
|
+
}
|
|
4503
|
+
function getSearchCredentialMeta(id) {
|
|
4504
|
+
const row = getDb().prepare("SELECT id, provider, label, key_blob, created_at, updated_at FROM search_credentials WHERE id = ?").get(id);
|
|
4505
|
+
if (!row) return null;
|
|
4506
|
+
return rowToMeta2(row);
|
|
4507
|
+
}
|
|
4508
|
+
function getSearchCredentialKey(id) {
|
|
4509
|
+
const row = getDb().prepare("SELECT key_blob FROM search_credentials WHERE id = ?").get(id);
|
|
4510
|
+
if (!row) return null;
|
|
4511
|
+
try {
|
|
4512
|
+
return decryptCredential(row.key_blob);
|
|
4513
|
+
} catch {
|
|
4514
|
+
return null;
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
function providerDisplayName2(provider) {
|
|
4518
|
+
switch (provider) {
|
|
4519
|
+
case "brave":
|
|
4520
|
+
return "Brave Search";
|
|
4521
|
+
case "tavily":
|
|
4522
|
+
return "Tavily Search";
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
function resolveFreeLabel2(provider, suggested) {
|
|
4526
|
+
const base = suggested && suggested.trim() || providerDisplayName2(provider);
|
|
4527
|
+
const existing = new Set(
|
|
4528
|
+
getDb().prepare("SELECT label FROM search_credentials").all().map((r) => r.label)
|
|
4529
|
+
);
|
|
4530
|
+
if (!existing.has(base)) return base;
|
|
4531
|
+
for (let n = 2; n < 1e3; n++) {
|
|
4532
|
+
const candidate = `${base} ${n}`;
|
|
4533
|
+
if (!existing.has(candidate)) return candidate;
|
|
4534
|
+
}
|
|
4535
|
+
return `${base} ${Date.now()}`;
|
|
4536
|
+
}
|
|
4537
|
+
function createSearchCredential(provider, label, plain) {
|
|
4538
|
+
const trimmed = plain.trim();
|
|
4539
|
+
if (!trimmed) return null;
|
|
4540
|
+
if (!isSearchProvider(provider)) return null;
|
|
4541
|
+
const resolvedLabel = resolveFreeLabel2(provider, label);
|
|
4542
|
+
const id = randomBytes3(8).toString("hex");
|
|
4543
|
+
const blob = encryptCredential(trimmed);
|
|
4544
|
+
const now = Date.now();
|
|
4545
|
+
getDb().prepare(
|
|
4546
|
+
`INSERT INTO search_credentials (id, provider, label, key_blob, created_at, updated_at)
|
|
4547
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
4548
|
+
).run(id, provider, resolvedLabel, blob, now, now);
|
|
4549
|
+
return getSearchCredentialMeta(id);
|
|
4550
|
+
}
|
|
4551
|
+
function deleteSearchCredential(id) {
|
|
4552
|
+
const meta = getSearchCredentialMeta(id);
|
|
4553
|
+
if (!meta) return null;
|
|
4554
|
+
getDb().prepare("DELETE FROM search_credentials WHERE id = ?").run(id);
|
|
4555
|
+
return meta.provider;
|
|
4556
|
+
}
|
|
4557
|
+
function resolveActiveSearchCredential() {
|
|
4558
|
+
const prefs = getPrefs();
|
|
4559
|
+
if (!prefs.activeSearchCredentialId) return null;
|
|
4560
|
+
return getSearchCredentialMeta(prefs.activeSearchCredentialId);
|
|
4561
|
+
}
|
|
4562
|
+
function getActiveSearchProvider() {
|
|
4563
|
+
return resolveActiveSearchCredential()?.provider ?? null;
|
|
4564
|
+
}
|
|
4565
|
+
function getActiveSearchKeyPlaintext() {
|
|
4566
|
+
const active = resolveActiveSearchCredential();
|
|
4567
|
+
if (!active) return null;
|
|
4568
|
+
return getSearchCredentialKey(active.id);
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
// src/storage/keys.ts
|
|
4210
4572
|
var SALT2 = "boardroom.v1.salt";
|
|
4211
4573
|
var ALGO2 = "aes-256-gcm";
|
|
4212
4574
|
var _key2 = null;
|
|
@@ -4216,14 +4578,14 @@ function deriveKey2() {
|
|
|
4216
4578
|
_key2 = scryptSync2(username, SALT2, 32);
|
|
4217
4579
|
return _key2;
|
|
4218
4580
|
}
|
|
4219
|
-
function
|
|
4220
|
-
const iv =
|
|
4581
|
+
function encrypt(plain) {
|
|
4582
|
+
const iv = randomBytes4(12);
|
|
4221
4583
|
const cipher = createCipheriv2(ALGO2, deriveKey2(), iv);
|
|
4222
4584
|
const ct = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
4223
4585
|
const tag = cipher.getAuthTag();
|
|
4224
4586
|
return Buffer.concat([iv, tag, ct]);
|
|
4225
4587
|
}
|
|
4226
|
-
function
|
|
4588
|
+
function decrypt(blob) {
|
|
4227
4589
|
const iv = blob.subarray(0, 12);
|
|
4228
4590
|
const tag = blob.subarray(12, 28);
|
|
4229
4591
|
const ct = blob.subarray(28);
|
|
@@ -4231,36 +4593,16 @@ function decrypt2(blob) {
|
|
|
4231
4593
|
decipher.setAuthTag(tag);
|
|
4232
4594
|
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
|
|
4233
4595
|
}
|
|
4234
|
-
function hasBraveKey() {
|
|
4235
|
-
const k = getKey("brave");
|
|
4236
|
-
return typeof k === "string" && k.length > 0;
|
|
4237
|
-
}
|
|
4238
|
-
function hasTavilyKey() {
|
|
4239
|
-
const k = getKey("tavily");
|
|
4240
|
-
return typeof k === "string" && k.length > 0;
|
|
4241
|
-
}
|
|
4242
4596
|
function hasWebSearchKey() {
|
|
4243
|
-
return
|
|
4244
|
-
}
|
|
4245
|
-
function resolveWebSearchBackend(preference) {
|
|
4246
|
-
const b = hasBraveKey();
|
|
4247
|
-
const t = hasTavilyKey();
|
|
4248
|
-
if (!b && !t) return null;
|
|
4249
|
-
if (b && !t) return "brave";
|
|
4250
|
-
if (!b && t) return "tavily";
|
|
4251
|
-
if (preference === "tavily" && t) return "tavily";
|
|
4252
|
-
if (preference === "brave" && b) return "brave";
|
|
4253
|
-
return b ? "brave" : "tavily";
|
|
4597
|
+
return getActiveSearchProvider() !== null;
|
|
4254
4598
|
}
|
|
4255
4599
|
function getActiveWebSearchCredentials() {
|
|
4256
|
-
const
|
|
4257
|
-
const preference = prefRaw === "tavily" ? "tavily" : "brave";
|
|
4258
|
-
const backend = resolveWebSearchBackend(preference);
|
|
4600
|
+
const backend = getActiveSearchProvider();
|
|
4259
4601
|
if (!backend) return null;
|
|
4260
|
-
const apiKey =
|
|
4602
|
+
const apiKey = getActiveSearchKeyPlaintext();
|
|
4261
4603
|
return apiKey ? { backend, apiKey } : null;
|
|
4262
4604
|
}
|
|
4263
|
-
function
|
|
4605
|
+
function maskKey(plain) {
|
|
4264
4606
|
const trimmed = plain.trim();
|
|
4265
4607
|
if (!trimmed) return "";
|
|
4266
4608
|
const n = trimmed.length;
|
|
@@ -4277,7 +4619,7 @@ function listKeyMeta() {
|
|
|
4277
4619
|
let preview = null;
|
|
4278
4620
|
if (r.key_blob.length > 0) {
|
|
4279
4621
|
try {
|
|
4280
|
-
preview =
|
|
4622
|
+
preview = maskKey(decrypt(r.key_blob));
|
|
4281
4623
|
} catch {
|
|
4282
4624
|
preview = null;
|
|
4283
4625
|
}
|
|
@@ -4295,7 +4637,7 @@ function getKey(provider) {
|
|
|
4295
4637
|
const row = getDb().prepare("SELECT key_blob FROM provider_keys WHERE provider = ?").get(provider);
|
|
4296
4638
|
if (!row) return null;
|
|
4297
4639
|
try {
|
|
4298
|
-
return
|
|
4640
|
+
return decrypt(row.key_blob);
|
|
4299
4641
|
} catch {
|
|
4300
4642
|
return null;
|
|
4301
4643
|
}
|
|
@@ -4306,7 +4648,7 @@ function setKey(provider, plain) {
|
|
|
4306
4648
|
deleteKey(provider);
|
|
4307
4649
|
return;
|
|
4308
4650
|
}
|
|
4309
|
-
const blob =
|
|
4651
|
+
const blob = encrypt(trimmed);
|
|
4310
4652
|
const now = Date.now();
|
|
4311
4653
|
getDb().prepare(
|
|
4312
4654
|
`INSERT INTO provider_keys (provider, key_blob, created_at, updated_at)
|
|
@@ -4324,8 +4666,10 @@ var PRIMARY_BY_CARRIER = {
|
|
|
4324
4666
|
bai: "haiku-4-5",
|
|
4325
4667
|
anthropic: "haiku-4-5",
|
|
4326
4668
|
openai: "gpt-5-4-mini",
|
|
4327
|
-
google: "gemini-3-1-flash"
|
|
4669
|
+
google: "gemini-3-1-flash",
|
|
4328
4670
|
// xai · no primary (no LLM modelV in registry as of 2026-05-17).
|
|
4671
|
+
moonshot: "kimi-k2-6",
|
|
4672
|
+
zhipu: "glm-5-1"
|
|
4329
4673
|
};
|
|
4330
4674
|
function reachableModelVs() {
|
|
4331
4675
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -4350,6 +4694,7 @@ function reconcileAgentModels(opts = {}) {
|
|
|
4350
4694
|
const carrier = activeCarrier();
|
|
4351
4695
|
const primary = carrier ? PRIMARY_BY_CARRIER[carrier] ?? null : null;
|
|
4352
4696
|
const forcePrimary = opts.forcePrimary === true;
|
|
4697
|
+
const priorCarrier = opts.priorCarrier ?? null;
|
|
4353
4698
|
const switched = [];
|
|
4354
4699
|
const cleared = [];
|
|
4355
4700
|
for (const agent of listAllAgents()) {
|
|
@@ -4357,12 +4702,29 @@ function reconcileAgentModels(opts = {}) {
|
|
|
4357
4702
|
if (agent.carrierPref) {
|
|
4358
4703
|
updateAgent(agent.id, { carrierPref: null });
|
|
4359
4704
|
}
|
|
4705
|
+
if (priorCarrier && v && priorCarrier !== carrier) {
|
|
4706
|
+
writeModelBucketEntry(agent.id, priorCarrier, v);
|
|
4707
|
+
}
|
|
4360
4708
|
if (!forcePrimary && v && reachable.has(v)) continue;
|
|
4361
4709
|
if (primary && carrier) {
|
|
4710
|
+
const bucket = getModelBucket(agent.id);
|
|
4711
|
+
const memorised = bucket[carrier];
|
|
4362
4712
|
const isChair = agent.roleKind === "moderator";
|
|
4363
|
-
|
|
4364
|
-
if (
|
|
4713
|
+
let target;
|
|
4714
|
+
if (memorised && reachable.has(memorised)) {
|
|
4715
|
+
target = memorised;
|
|
4716
|
+
} else {
|
|
4717
|
+
if (memorised) deleteModelBucketEntry(agent.id, carrier);
|
|
4718
|
+
target = isChair ? primary : pickRandomFastModel(carrier) ?? primary;
|
|
4719
|
+
}
|
|
4720
|
+
if (v === target) {
|
|
4721
|
+
if (bucket[carrier] !== target) {
|
|
4722
|
+
writeModelBucketEntry(agent.id, carrier, target);
|
|
4723
|
+
}
|
|
4724
|
+
continue;
|
|
4725
|
+
}
|
|
4365
4726
|
updateAgent(agent.id, { modelV: target });
|
|
4727
|
+
writeModelBucketEntry(agent.id, carrier, target);
|
|
4366
4728
|
switched.push(agent.id);
|
|
4367
4729
|
} else {
|
|
4368
4730
|
if (v === "") continue;
|
|
@@ -4493,8 +4855,15 @@ var FAST_POOL_BY_CARRIER = {
|
|
|
4493
4855
|
],
|
|
4494
4856
|
anthropic: ["opus-4-6-fast", "haiku-4-5"],
|
|
4495
4857
|
openai: ["gpt-5-4-mini"],
|
|
4496
|
-
google: ["gemini-3-flash", "gemini-3-1-flash"]
|
|
4858
|
+
google: ["gemini-3-flash", "gemini-3-1-flash"],
|
|
4497
4859
|
// xai · no fast pool (no LLM modelV in registry).
|
|
4860
|
+
// Moonshot / Zhipu · single-entry pools because the registry only
|
|
4861
|
+
// carries one LLM modelV per provider today. Every director on this
|
|
4862
|
+
// carrier ends up on the same model (no brand variety), which is fine
|
|
4863
|
+
// for these single-model providers · adding more Kimi / GLM rows
|
|
4864
|
+
// to the registry would naturally extend the pool.
|
|
4865
|
+
moonshot: ["kimi-k2-6"],
|
|
4866
|
+
zhipu: ["glm-5-1"]
|
|
4498
4867
|
};
|
|
4499
4868
|
function pickRandomFastModel(carrier) {
|
|
4500
4869
|
if (!carrier) return null;
|
|
@@ -5156,6 +5525,10 @@ function flagshipCandidates() {
|
|
|
5156
5525
|
return out;
|
|
5157
5526
|
}
|
|
5158
5527
|
async function callPhaseLLM(state, modelV, messages, opts) {
|
|
5528
|
+
const r = await callPhaseLLMVerbose(state, modelV, messages, opts);
|
|
5529
|
+
return r ? r.text : null;
|
|
5530
|
+
}
|
|
5531
|
+
async function callPhaseLLMVerbose(state, modelV, messages, opts) {
|
|
5159
5532
|
if (!isModelV(modelV)) return null;
|
|
5160
5533
|
const t = signalWithTimeout(state.controller.signal, LLM_CALL_TIMEOUT_MS);
|
|
5161
5534
|
try {
|
|
@@ -5173,7 +5546,7 @@ async function callPhaseLLM(state, modelV, messages, opts) {
|
|
|
5173
5546
|
return null;
|
|
5174
5547
|
}
|
|
5175
5548
|
}
|
|
5176
|
-
return r.text;
|
|
5549
|
+
return { text: r.text, finishReason: r.finishReason };
|
|
5177
5550
|
} catch (e) {
|
|
5178
5551
|
process.stderr.write(`[persona-builder] ${modelV} failed: ${e instanceof Error ? e.message : String(e)}
|
|
5179
5552
|
`);
|
|
@@ -5351,7 +5724,9 @@ async function runPipeline(state) {
|
|
|
5351
5724
|
if (!profileV1) {
|
|
5352
5725
|
const status = checkAbortOrCap();
|
|
5353
5726
|
if (status === "aborted") return finalizeAbort(state);
|
|
5354
|
-
|
|
5727
|
+
const candidatesTried = flagshipCandidates();
|
|
5728
|
+
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`;
|
|
5729
|
+
return fail(`Phase 1 (persona spec) failed \xB7 ${hint}.`);
|
|
5355
5730
|
}
|
|
5356
5731
|
partial.profileV1 = profileV1;
|
|
5357
5732
|
partial.spec = toCore(profileV1);
|
|
@@ -5503,17 +5878,58 @@ function finalizeFromCheck(state, status) {
|
|
|
5503
5878
|
}
|
|
5504
5879
|
async function runProfilePass(state, label, webContext) {
|
|
5505
5880
|
const messages = buildAgentProfileMessages({ description: state.description, webContext: webContext ?? null });
|
|
5506
|
-
|
|
5881
|
+
const candidates = flagshipCandidates();
|
|
5882
|
+
if (candidates.length === 0) {
|
|
5883
|
+
process.stderr.write(`[persona-builder/${label}] no reachable models for the active credential \xB7 cannot build profile
|
|
5884
|
+
`);
|
|
5885
|
+
return null;
|
|
5886
|
+
}
|
|
5887
|
+
const PROFILE_ATTEMPTS_PER_MODEL = 2;
|
|
5888
|
+
const PROFILE_MAX_TOKENS_BASE = 4096;
|
|
5889
|
+
const PROFILE_MAX_TOKENS_ESCALATED = 6500;
|
|
5890
|
+
for (const modelV of candidates) {
|
|
5507
5891
|
if (state.controller.signal.aborted) return null;
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5892
|
+
let truncatedLastAttempt = false;
|
|
5893
|
+
for (let attempt = 1; attempt <= PROFILE_ATTEMPTS_PER_MODEL; attempt++) {
|
|
5894
|
+
if (state.controller.signal.aborted) return null;
|
|
5895
|
+
const temperature = attempt === 1 ? 0.6 : 0.8;
|
|
5896
|
+
const maxTokens = truncatedLastAttempt ? PROFILE_MAX_TOKENS_ESCALATED : PROFILE_MAX_TOKENS_BASE;
|
|
5897
|
+
const result = await callPhaseLLMVerbose(state, modelV, messages, { temperature, maxTokens });
|
|
5898
|
+
if (!result || !result.text) {
|
|
5899
|
+
process.stderr.write(`[persona-builder/${label}] ${modelV} attempt ${attempt}/${PROFILE_ATTEMPTS_PER_MODEL} returned no text
|
|
5513
5900
|
`);
|
|
5901
|
+
truncatedLastAttempt = false;
|
|
5902
|
+
continue;
|
|
5903
|
+
}
|
|
5904
|
+
const raw = result.text;
|
|
5905
|
+
const parsed = parseAgentProfile(raw);
|
|
5906
|
+
if (parsed) return parsed;
|
|
5907
|
+
const truncated = result.finishReason === "length" || looksTruncated(raw);
|
|
5908
|
+
truncatedLastAttempt = truncated;
|
|
5909
|
+
const head = raw.slice(0, 200).replace(/\s+/g, " ");
|
|
5910
|
+
const tail = raw.length > 200 ? raw.slice(-160).replace(/\s+/g, " ") : "";
|
|
5911
|
+
process.stderr.write(
|
|
5912
|
+
`[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}` : ""}
|
|
5913
|
+
`
|
|
5914
|
+
);
|
|
5915
|
+
}
|
|
5514
5916
|
}
|
|
5515
5917
|
return null;
|
|
5516
5918
|
}
|
|
5919
|
+
function looksTruncated(raw) {
|
|
5920
|
+
if (!raw) return false;
|
|
5921
|
+
const fenceMatch = /```(?:json)?\s*([\s\S]*)$/i.exec(raw);
|
|
5922
|
+
const body = fenceMatch ? fenceMatch[1] : raw;
|
|
5923
|
+
if (!body) return false;
|
|
5924
|
+
const start = body.indexOf("{");
|
|
5925
|
+
if (start === -1) return false;
|
|
5926
|
+
let depth = 0;
|
|
5927
|
+
for (let i = start; i < body.length; i++) {
|
|
5928
|
+
if (body[i] === "{") depth++;
|
|
5929
|
+
else if (body[i] === "}") depth--;
|
|
5930
|
+
}
|
|
5931
|
+
return depth > 0;
|
|
5932
|
+
}
|
|
5517
5933
|
async function runReActLoop(state, profileV1, reportProgress) {
|
|
5518
5934
|
const creds = getActiveWebSearchCredentials();
|
|
5519
5935
|
if (!creds) {
|
|
@@ -6159,12 +6575,12 @@ async function generateCelebritySeed(opts) {
|
|
|
6159
6575
|
init_db();
|
|
6160
6576
|
|
|
6161
6577
|
// src/utils/id.ts
|
|
6162
|
-
import { randomBytes as
|
|
6578
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
6163
6579
|
var ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
6164
6580
|
var ALPHABET_LEN = ALPHABET.length;
|
|
6165
6581
|
var MASK = (1 << 5) - 1;
|
|
6166
6582
|
function newId(len = 12) {
|
|
6167
|
-
const bytes =
|
|
6583
|
+
const bytes = randomBytes5(len);
|
|
6168
6584
|
let out = "";
|
|
6169
6585
|
for (let i = 0; i < len; i++) {
|
|
6170
6586
|
out += ALPHABET[bytes[i] & MASK];
|
|
@@ -8113,7 +8529,14 @@ function agentsRouter() {
|
|
|
8113
8529
|
modelV,
|
|
8114
8530
|
messages: profileMessages,
|
|
8115
8531
|
temperature: 0.6,
|
|
8116
|
-
|
|
8532
|
+
// Bumped from 1600 → 4096 · the profile JSON plus any
|
|
8533
|
+
// chain-of-thought / reasoning preamble on Kimi / GLM /
|
|
8534
|
+
// DeepSeek-R lines easily overran the smaller ceiling on
|
|
8535
|
+
// OpenRouter routes, which surface truncation as
|
|
8536
|
+
// "unparseable JSON" (the closing `}` is missing). Quick
|
|
8537
|
+
// mode is single-shot — no retry — so the ceiling has to
|
|
8538
|
+
// be generous up front.
|
|
8539
|
+
maxTokens: 4096,
|
|
8117
8540
|
signal
|
|
8118
8541
|
});
|
|
8119
8542
|
const parsed = parseAgentProfile(raw);
|
|
@@ -8121,6 +8544,8 @@ function agentsRouter() {
|
|
|
8121
8544
|
profile = parsed;
|
|
8122
8545
|
break;
|
|
8123
8546
|
}
|
|
8547
|
+
process.stderr.write(`[agent-spec/profile] ${modelV} returned unparseable profile \xB7 len=${raw.length} head: ${raw.slice(0, 200).replace(/\s+/g, " ")}
|
|
8548
|
+
`);
|
|
8124
8549
|
} catch (e) {
|
|
8125
8550
|
process.stderr.write(`[agent-spec/profile] ${modelV} failed: ${e instanceof Error ? e.message : String(e)}
|
|
8126
8551
|
`);
|
|
@@ -8494,7 +8919,7 @@ function agentsRouter() {
|
|
|
8494
8919
|
patch.carrierPref = null;
|
|
8495
8920
|
} else if (typeof b.carrierPref === "string") {
|
|
8496
8921
|
const v = b.carrierPref.trim();
|
|
8497
|
-
const allowed = /* @__PURE__ */ new Set(["openrouter", "bai", "anthropic", "openai", "google", "xai"]);
|
|
8922
|
+
const allowed = /* @__PURE__ */ new Set(["openrouter", "bai", "anthropic", "openai", "google", "xai", "moonshot", "zhipu"]);
|
|
8498
8923
|
if (!allowed.has(v)) {
|
|
8499
8924
|
return c.json({ error: `unknown carrier: ${v}` }, 400);
|
|
8500
8925
|
}
|
|
@@ -8546,6 +8971,42 @@ function agentsRouter() {
|
|
|
8546
8971
|
patch.isPinned = b.isPinned;
|
|
8547
8972
|
}
|
|
8548
8973
|
const updated = updateAgent(id, patch);
|
|
8974
|
+
if (updated) {
|
|
8975
|
+
if (patch.modelV !== void 0) {
|
|
8976
|
+
const carrier = activeCarrier();
|
|
8977
|
+
if (carrier) {
|
|
8978
|
+
try {
|
|
8979
|
+
writeModelBucketEntry(id, carrier, patch.modelV);
|
|
8980
|
+
} catch (e) {
|
|
8981
|
+
process.stderr.write(
|
|
8982
|
+
`[agents.patch] model bucket write failed for ${id}: ${e instanceof Error ? e.message : String(e)}
|
|
8983
|
+
`
|
|
8984
|
+
);
|
|
8985
|
+
}
|
|
8986
|
+
}
|
|
8987
|
+
}
|
|
8988
|
+
if ("voice" in patch) {
|
|
8989
|
+
if (patch.voice && updated.voice && (updated.voice.provider === "minimax" || updated.voice.provider === "elevenlabs")) {
|
|
8990
|
+
try {
|
|
8991
|
+
writeVoiceBucketEntry(id, updated.voice.provider, updated.voice);
|
|
8992
|
+
} catch (e) {
|
|
8993
|
+
process.stderr.write(
|
|
8994
|
+
`[agents.patch] voice bucket write failed for ${id}: ${e instanceof Error ? e.message : String(e)}
|
|
8995
|
+
`
|
|
8996
|
+
);
|
|
8997
|
+
}
|
|
8998
|
+
} else if (!patch.voice && existing.voice && (existing.voice.provider === "minimax" || existing.voice.provider === "elevenlabs")) {
|
|
8999
|
+
try {
|
|
9000
|
+
deleteVoiceBucketEntry(id, existing.voice.provider);
|
|
9001
|
+
} catch (e) {
|
|
9002
|
+
process.stderr.write(
|
|
9003
|
+
`[agents.patch] voice bucket delete failed for ${id}: ${e instanceof Error ? e.message : String(e)}
|
|
9004
|
+
`
|
|
9005
|
+
);
|
|
9006
|
+
}
|
|
9007
|
+
}
|
|
9008
|
+
}
|
|
9009
|
+
}
|
|
8549
9010
|
return c.json(updated);
|
|
8550
9011
|
});
|
|
8551
9012
|
r.delete("/:id", (c) => {
|
|
@@ -13704,7 +14165,7 @@ function cleanupOrphanedStreams(opts = {}) {
|
|
|
13704
14165
|
|
|
13705
14166
|
// src/storage/rooms.ts
|
|
13706
14167
|
init_db();
|
|
13707
|
-
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";
|
|
14168
|
+
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";
|
|
13708
14169
|
function mapRow8(row) {
|
|
13709
14170
|
return {
|
|
13710
14171
|
id: row.id,
|
|
@@ -13724,7 +14185,8 @@ function mapRow8(row) {
|
|
|
13724
14185
|
adjournedAt: row.adjourned_at,
|
|
13725
14186
|
incognito: row.incognito === 1,
|
|
13726
14187
|
parentRoomId: row.parent_room_id,
|
|
13727
|
-
parentBriefId: row.parent_brief_id
|
|
14188
|
+
parentBriefId: row.parent_brief_id,
|
|
14189
|
+
nameAuto: row.name_auto === 1
|
|
13728
14190
|
};
|
|
13729
14191
|
}
|
|
13730
14192
|
function mapMember(row) {
|
|
@@ -13788,7 +14250,7 @@ function createRoom(input) {
|
|
|
13788
14250
|
const briefStyle = input.briefStyle ?? "auto";
|
|
13789
14251
|
const deliveryMode = input.deliveryMode === "voice" ? "voice" : "text";
|
|
13790
14252
|
const insertRoom = db.prepare(
|
|
13791
|
-
"INSERT INTO rooms (id, number, name, subject, mode, intensity, delivery_mode, brief_style, status, created_at, parent_room_id, parent_brief_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'live', ?, ?, ?)"
|
|
14253
|
+
"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', ?, ?, ?, ?)"
|
|
13792
14254
|
);
|
|
13793
14255
|
const insertMember = db.prepare(
|
|
13794
14256
|
"INSERT INTO room_members (room_id, agent_id, position, joined_at) VALUES (?, ?, ?, ?)"
|
|
@@ -13796,8 +14258,9 @@ function createRoom(input) {
|
|
|
13796
14258
|
const chair = getChairAgent();
|
|
13797
14259
|
const parentRoomId = input.parentRoomId && input.parentRoomId.trim() ? input.parentRoomId.trim() : null;
|
|
13798
14260
|
const parentBriefId = input.parentBriefId && input.parentBriefId.trim() ? input.parentBriefId.trim() : null;
|
|
14261
|
+
const nameAuto = input.nameAuto === false ? 0 : 1;
|
|
13799
14262
|
const tx = db.transaction(() => {
|
|
13800
|
-
insertRoom.run(id, number, input.name, input.subject, mode, intensity, deliveryMode, briefStyle, now, parentRoomId, parentBriefId);
|
|
14263
|
+
insertRoom.run(id, number, input.name, input.subject, mode, intensity, deliveryMode, briefStyle, now, parentRoomId, parentBriefId, nameAuto);
|
|
13801
14264
|
if (chair) insertMember.run(id, chair.id, -1, now);
|
|
13802
14265
|
input.agentIds.forEach((agentId, idx) => {
|
|
13803
14266
|
if (chair && agentId === chair.id) return;
|
|
@@ -13824,6 +14287,12 @@ function setRoomStatus(roomId, status, ts = {}) {
|
|
|
13824
14287
|
vals.push(roomId);
|
|
13825
14288
|
getDb().prepare(`UPDATE rooms SET ${sets.join(", ")} WHERE id = ?`).run(...vals);
|
|
13826
14289
|
}
|
|
14290
|
+
function setRoomNameFromAuto(roomId, name) {
|
|
14291
|
+
const trimmed = name.trim();
|
|
14292
|
+
if (!trimmed) return false;
|
|
14293
|
+
const r = getDb().prepare("UPDATE rooms SET name = ? WHERE id = ? AND name_auto = 1").run(trimmed, roomId);
|
|
14294
|
+
return r.changes > 0;
|
|
14295
|
+
}
|
|
13827
14296
|
function addRoomMember(roomId, agentId) {
|
|
13828
14297
|
const db = getDb();
|
|
13829
14298
|
const existing = db.prepare("SELECT agent_id, position, joined_at, removed_at FROM room_members WHERE room_id = ? AND agent_id = ?").get(roomId, agentId);
|
|
@@ -14343,6 +14812,103 @@ import { Buffer as Buffer2 } from "buffer";
|
|
|
14343
14812
|
init_db();
|
|
14344
14813
|
import { createHash } from "crypto";
|
|
14345
14814
|
|
|
14815
|
+
// src/storage/voice-credentials.ts
|
|
14816
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
14817
|
+
init_db();
|
|
14818
|
+
var ALL_VOICE_PROVIDERS = ["minimax", "elevenlabs"];
|
|
14819
|
+
function isVoiceProvider(p) {
|
|
14820
|
+
return ALL_VOICE_PROVIDERS.includes(p);
|
|
14821
|
+
}
|
|
14822
|
+
var VOICE_PROVIDER_PRIORITY = ["minimax", "elevenlabs"];
|
|
14823
|
+
function rowToMeta3(row) {
|
|
14824
|
+
if (!isVoiceProvider(row.provider)) return null;
|
|
14825
|
+
let preview = "";
|
|
14826
|
+
try {
|
|
14827
|
+
preview = maskCredential(decryptCredential(row.key_blob));
|
|
14828
|
+
} catch {
|
|
14829
|
+
preview = "";
|
|
14830
|
+
}
|
|
14831
|
+
return {
|
|
14832
|
+
id: row.id,
|
|
14833
|
+
provider: row.provider,
|
|
14834
|
+
label: row.label,
|
|
14835
|
+
preview,
|
|
14836
|
+
createdAt: row.created_at,
|
|
14837
|
+
updatedAt: row.updated_at
|
|
14838
|
+
};
|
|
14839
|
+
}
|
|
14840
|
+
function listVoiceCredentials() {
|
|
14841
|
+
const rows = getDb().prepare("SELECT id, provider, label, key_blob, created_at, updated_at FROM voice_credentials ORDER BY created_at ASC").all();
|
|
14842
|
+
return rows.map(rowToMeta3).filter((m) => m !== null);
|
|
14843
|
+
}
|
|
14844
|
+
function getVoiceCredentialMeta(id) {
|
|
14845
|
+
const row = getDb().prepare("SELECT id, provider, label, key_blob, created_at, updated_at FROM voice_credentials WHERE id = ?").get(id);
|
|
14846
|
+
if (!row) return null;
|
|
14847
|
+
return rowToMeta3(row);
|
|
14848
|
+
}
|
|
14849
|
+
function getVoiceCredentialKey(id) {
|
|
14850
|
+
const row = getDb().prepare("SELECT key_blob FROM voice_credentials WHERE id = ?").get(id);
|
|
14851
|
+
if (!row) return null;
|
|
14852
|
+
try {
|
|
14853
|
+
return decryptCredential(row.key_blob);
|
|
14854
|
+
} catch {
|
|
14855
|
+
return null;
|
|
14856
|
+
}
|
|
14857
|
+
}
|
|
14858
|
+
function providerDisplayName3(provider) {
|
|
14859
|
+
switch (provider) {
|
|
14860
|
+
case "minimax":
|
|
14861
|
+
return "MiniMax";
|
|
14862
|
+
case "elevenlabs":
|
|
14863
|
+
return "ElevenLabs";
|
|
14864
|
+
}
|
|
14865
|
+
}
|
|
14866
|
+
function resolveFreeLabel3(provider, suggested) {
|
|
14867
|
+
const base = suggested && suggested.trim() || providerDisplayName3(provider);
|
|
14868
|
+
const existing = new Set(
|
|
14869
|
+
getDb().prepare("SELECT label FROM voice_credentials").all().map((r) => r.label)
|
|
14870
|
+
);
|
|
14871
|
+
if (!existing.has(base)) return base;
|
|
14872
|
+
for (let n = 2; n < 1e3; n++) {
|
|
14873
|
+
const candidate = `${base} ${n}`;
|
|
14874
|
+
if (!existing.has(candidate)) return candidate;
|
|
14875
|
+
}
|
|
14876
|
+
return `${base} ${Date.now()}`;
|
|
14877
|
+
}
|
|
14878
|
+
function createVoiceCredential(provider, label, plain) {
|
|
14879
|
+
const trimmed = plain.trim();
|
|
14880
|
+
if (!trimmed) return null;
|
|
14881
|
+
if (!isVoiceProvider(provider)) return null;
|
|
14882
|
+
const resolvedLabel = resolveFreeLabel3(provider, label);
|
|
14883
|
+
const id = randomBytes6(8).toString("hex");
|
|
14884
|
+
const blob = encryptCredential(trimmed);
|
|
14885
|
+
const now = Date.now();
|
|
14886
|
+
getDb().prepare(
|
|
14887
|
+
`INSERT INTO voice_credentials (id, provider, label, key_blob, created_at, updated_at)
|
|
14888
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
14889
|
+
).run(id, provider, resolvedLabel, blob, now, now);
|
|
14890
|
+
return getVoiceCredentialMeta(id);
|
|
14891
|
+
}
|
|
14892
|
+
function deleteVoiceCredential(id) {
|
|
14893
|
+
const meta = getVoiceCredentialMeta(id);
|
|
14894
|
+
if (!meta) return null;
|
|
14895
|
+
getDb().prepare("DELETE FROM voice_credentials WHERE id = ?").run(id);
|
|
14896
|
+
return meta.provider;
|
|
14897
|
+
}
|
|
14898
|
+
function resolveActiveVoiceCredential() {
|
|
14899
|
+
const prefs = getPrefs();
|
|
14900
|
+
if (!prefs.activeVoiceCredentialId) return null;
|
|
14901
|
+
return getVoiceCredentialMeta(prefs.activeVoiceCredentialId);
|
|
14902
|
+
}
|
|
14903
|
+
function getActiveVoiceProvider() {
|
|
14904
|
+
return resolveActiveVoiceCredential()?.provider ?? null;
|
|
14905
|
+
}
|
|
14906
|
+
function getActiveVoiceKeyPlaintext() {
|
|
14907
|
+
const active = resolveActiveVoiceCredential();
|
|
14908
|
+
if (!active) return null;
|
|
14909
|
+
return getVoiceCredentialKey(active.id);
|
|
14910
|
+
}
|
|
14911
|
+
|
|
14346
14912
|
// src/voice/registry.ts
|
|
14347
14913
|
function minimaxBaseUrl() {
|
|
14348
14914
|
const region = getPrefs().minimaxRegion;
|
|
@@ -14387,10 +14953,12 @@ function listConfiguredVoices() {
|
|
|
14387
14953
|
const out = [];
|
|
14388
14954
|
const openaiReady = !!getKey("openai");
|
|
14389
14955
|
if (openaiReady) out.push(...OPENAI_VOICES.map((v) => ({ ...v, configured: true })));
|
|
14390
|
-
const
|
|
14391
|
-
if (
|
|
14392
|
-
|
|
14393
|
-
if (
|
|
14956
|
+
const activeProvider = getActiveVoiceProvider();
|
|
14957
|
+
if (activeProvider === "minimax") {
|
|
14958
|
+
out.push(...MINIMAX_SYSTEM_VOICES.map((v) => ({ ...v, configured: true })));
|
|
14959
|
+
} else if (activeProvider === "elevenlabs") {
|
|
14960
|
+
out.push(...ELEVENLABS_DEFAULT_VOICES.map((v) => ({ ...v, configured: true })));
|
|
14961
|
+
}
|
|
14394
14962
|
out.push({
|
|
14395
14963
|
provider: "browser",
|
|
14396
14964
|
model: "speechSynthesis",
|
|
@@ -14401,14 +14969,21 @@ function listConfiguredVoices() {
|
|
|
14401
14969
|
return out;
|
|
14402
14970
|
}
|
|
14403
14971
|
async function listAvailableVoices() {
|
|
14972
|
+
const activeProvider = getActiveVoiceProvider();
|
|
14404
14973
|
let voices = listConfiguredVoices();
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14974
|
+
if (!activeProvider) {
|
|
14975
|
+
return { voices, provider: null, configured: false };
|
|
14976
|
+
}
|
|
14977
|
+
const activeKey = getActiveVoiceKeyPlaintext();
|
|
14978
|
+
if (!activeKey) {
|
|
14979
|
+
return { voices, provider: activeProvider, configured: false };
|
|
14980
|
+
}
|
|
14981
|
+
if (activeProvider === "minimax") {
|
|
14982
|
+
try {
|
|
14408
14983
|
const res = await fetch(`${minimaxBaseUrl()}/v1/get_voice`, {
|
|
14409
14984
|
method: "POST",
|
|
14410
14985
|
headers: {
|
|
14411
|
-
"authorization": `Bearer ${
|
|
14986
|
+
"authorization": `Bearer ${activeKey}`,
|
|
14412
14987
|
"content-type": "application/json"
|
|
14413
14988
|
},
|
|
14414
14989
|
body: JSON.stringify({ voice_type: "all" })
|
|
@@ -14437,9 +15012,9 @@ async function listAvailableVoices() {
|
|
|
14437
15012
|
}
|
|
14438
15013
|
} catch {
|
|
14439
15014
|
}
|
|
15015
|
+
return { voices, provider: "minimax", configured: true };
|
|
14440
15016
|
}
|
|
14441
|
-
|
|
14442
|
-
if (elKey) {
|
|
15017
|
+
if (activeProvider === "elevenlabs") {
|
|
14443
15018
|
const personal = [];
|
|
14444
15019
|
const shared = [];
|
|
14445
15020
|
await Promise.all([
|
|
@@ -14447,7 +15022,7 @@ async function listAvailableVoices() {
|
|
|
14447
15022
|
try {
|
|
14448
15023
|
const res = await fetch(
|
|
14449
15024
|
"https://api.elevenlabs.io/v1/voices?show_legacy=true&include_total_count=true",
|
|
14450
|
-
{ headers: { "xi-api-key":
|
|
15025
|
+
{ headers: { "xi-api-key": activeKey } }
|
|
14451
15026
|
);
|
|
14452
15027
|
if (!res.ok) {
|
|
14453
15028
|
const errText = await res.text();
|
|
@@ -14475,7 +15050,7 @@ async function listAvailableVoices() {
|
|
|
14475
15050
|
try {
|
|
14476
15051
|
const res = await fetch(
|
|
14477
15052
|
"https://api.elevenlabs.io/v1/shared-voices?page_size=100",
|
|
14478
|
-
{ headers: { "xi-api-key":
|
|
15053
|
+
{ headers: { "xi-api-key": activeKey } }
|
|
14479
15054
|
);
|
|
14480
15055
|
if (!res.ok) {
|
|
14481
15056
|
const errText = await res.text();
|
|
@@ -14528,8 +15103,9 @@ async function listAvailableVoices() {
|
|
|
14528
15103
|
}));
|
|
14529
15104
|
voices = [...nonEl, ...personalMapped, ...sharedMapped];
|
|
14530
15105
|
}
|
|
15106
|
+
return { voices, provider: "elevenlabs", configured: true };
|
|
14531
15107
|
}
|
|
14532
|
-
return voices;
|
|
15108
|
+
return { voices, provider: activeProvider, configured: true };
|
|
14533
15109
|
}
|
|
14534
15110
|
function elevenLabsSharedVoiceRows(raw) {
|
|
14535
15111
|
if (!Array.isArray(raw)) return [];
|
|
@@ -14578,6 +15154,10 @@ function defaultVoiceForProvider(provider) {
|
|
|
14578
15154
|
}
|
|
14579
15155
|
|
|
14580
15156
|
// src/voice/tts.ts
|
|
15157
|
+
function activeVoiceKeyFor(wanted) {
|
|
15158
|
+
if (getActiveVoiceProvider() !== wanted) return null;
|
|
15159
|
+
return getActiveVoiceKeyPlaintext();
|
|
15160
|
+
}
|
|
14581
15161
|
function minimaxBaseUrl2() {
|
|
14582
15162
|
const region = getPrefs().minimaxRegion;
|
|
14583
15163
|
return region === "intl" ? "https://api.minimax.io" : "https://api.minimaxi.com";
|
|
@@ -14642,9 +15222,26 @@ function cleanForSpeech(md) {
|
|
|
14642
15222
|
return out.trim();
|
|
14643
15223
|
}
|
|
14644
15224
|
function voiceProfileForAgent(agent) {
|
|
14645
|
-
|
|
15225
|
+
const activeProvider = getActiveVoiceProvider();
|
|
15226
|
+
if (agent.voice && agent.voice.provider === activeProvider) {
|
|
15227
|
+
return agent.voice;
|
|
15228
|
+
}
|
|
15229
|
+
if (agent.voice && activeProvider) {
|
|
15230
|
+
const fresh = defaultVoiceForProvider(activeProvider);
|
|
15231
|
+
if (fresh) {
|
|
15232
|
+
return {
|
|
15233
|
+
provider: fresh.provider,
|
|
15234
|
+
model: fresh.model,
|
|
15235
|
+
voiceId: fresh.voiceId,
|
|
15236
|
+
speed: agent.voice.speed ?? 1,
|
|
15237
|
+
pitch: agent.voice.pitch ?? 0,
|
|
15238
|
+
volume: agent.voice.volume ?? 1,
|
|
15239
|
+
...agent.voice.emotion ? { emotion: agent.voice.emotion } : {}
|
|
15240
|
+
};
|
|
15241
|
+
}
|
|
15242
|
+
}
|
|
14646
15243
|
const fallback = defaultVoiceForProvider(
|
|
14647
|
-
|
|
15244
|
+
activeProvider ?? (getKey("openai") ? "openai" : "browser")
|
|
14648
15245
|
);
|
|
14649
15246
|
return {
|
|
14650
15247
|
provider: fallback?.provider ?? "browser",
|
|
@@ -14667,15 +15264,16 @@ async function synthesizeSpeech(text, profile, signal) {
|
|
|
14667
15264
|
};
|
|
14668
15265
|
}
|
|
14669
15266
|
async function* synthesizeSpeechStream(text, profile, signal) {
|
|
14670
|
-
if (profile.provider === "elevenlabs" &&
|
|
15267
|
+
if (profile.provider === "elevenlabs" && activeVoiceKeyFor("elevenlabs")) {
|
|
14671
15268
|
yield* synthesizeElevenLabsStream(text, profile, signal);
|
|
14672
15269
|
return;
|
|
14673
15270
|
}
|
|
14674
|
-
|
|
15271
|
+
const minimaxKey = activeVoiceKeyFor("minimax");
|
|
15272
|
+
if (profile.provider !== "minimax" || !minimaxKey) {
|
|
14675
15273
|
yield await synthesizeSpeech(text, profile, signal);
|
|
14676
15274
|
return;
|
|
14677
15275
|
}
|
|
14678
|
-
const key =
|
|
15276
|
+
const key = minimaxKey;
|
|
14679
15277
|
const model = profile.model || "speech-2.8-hd";
|
|
14680
15278
|
const res = await fetch(`${minimaxBaseUrl2()}/v1/t2a_v2`, {
|
|
14681
15279
|
method: "POST",
|
|
@@ -14791,7 +15389,7 @@ async function* synthesizeSpeechStream(text, profile, signal) {
|
|
|
14791
15389
|
}
|
|
14792
15390
|
}
|
|
14793
15391
|
async function synthesizeMiniMax(text, profile, signal) {
|
|
14794
|
-
const key =
|
|
15392
|
+
const key = activeVoiceKeyFor("minimax");
|
|
14795
15393
|
if (!key) {
|
|
14796
15394
|
return { provider: "browser", model: "speechSynthesis", voiceId: "system-default", text };
|
|
14797
15395
|
}
|
|
@@ -14901,7 +15499,7 @@ async function synthesizeOpenAI(text, profile, signal) {
|
|
|
14901
15499
|
};
|
|
14902
15500
|
}
|
|
14903
15501
|
async function synthesizeElevenLabs(text, profile, signal) {
|
|
14904
|
-
const key =
|
|
15502
|
+
const key = activeVoiceKeyFor("elevenlabs");
|
|
14905
15503
|
if (!key) {
|
|
14906
15504
|
return { provider: "browser", model: "speechSynthesis", voiceId: "system-default", text };
|
|
14907
15505
|
}
|
|
@@ -14946,7 +15544,7 @@ async function synthesizeElevenLabs(text, profile, signal) {
|
|
|
14946
15544
|
};
|
|
14947
15545
|
}
|
|
14948
15546
|
async function* synthesizeElevenLabsStream(text, profile, signal) {
|
|
14949
|
-
const key =
|
|
15547
|
+
const key = activeVoiceKeyFor("elevenlabs");
|
|
14950
15548
|
if (!key) {
|
|
14951
15549
|
yield await synthesizeSpeech(text, profile, signal);
|
|
14952
15550
|
return;
|
|
@@ -16910,6 +17508,7 @@ function credentialsRouter() {
|
|
|
16910
17508
|
} else {
|
|
16911
17509
|
return c.json({ error: "id must be a string or null" }, 400);
|
|
16912
17510
|
}
|
|
17511
|
+
const priorCarrier = activeCarrier();
|
|
16913
17512
|
if (nextId) {
|
|
16914
17513
|
const meta = getLlmCredentialMeta(nextId);
|
|
16915
17514
|
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
@@ -16920,7 +17519,7 @@ function credentialsRouter() {
|
|
|
16920
17519
|
updatePrefs({ activeLlmCredentialId: null });
|
|
16921
17520
|
}
|
|
16922
17521
|
try {
|
|
16923
|
-
reconcileAgentModels({ forcePrimary: true });
|
|
17522
|
+
reconcileAgentModels({ forcePrimary: true, priorCarrier });
|
|
16924
17523
|
} catch (e) {
|
|
16925
17524
|
process.stderr.write(`[credentials.active] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
16926
17525
|
`);
|
|
@@ -16952,7 +17551,7 @@ function credentialsRouter() {
|
|
|
16952
17551
|
const flagship = PRIMARY_BY_CARRIER[provider];
|
|
16953
17552
|
if (flagship) updatePrefs({ defaultModelV: flagship });
|
|
16954
17553
|
try {
|
|
16955
|
-
reconcileAgentModels({ forcePrimary: true });
|
|
17554
|
+
reconcileAgentModels({ forcePrimary: true, priorCarrier: null });
|
|
16956
17555
|
} catch (e) {
|
|
16957
17556
|
process.stderr.write(`[credentials.post] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
16958
17557
|
`);
|
|
@@ -16967,6 +17566,7 @@ function credentialsRouter() {
|
|
|
16967
17566
|
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
16968
17567
|
const prefs = getPrefs();
|
|
16969
17568
|
const wasActive = prefs.activeLlmCredentialId === id;
|
|
17569
|
+
const priorCarrier = wasActive ? activeCarrier() : null;
|
|
16970
17570
|
const removedProvider = deleteLlmCredential(id);
|
|
16971
17571
|
if (wasActive) {
|
|
16972
17572
|
const nextId = pickNextActiveId(removedProvider);
|
|
@@ -16982,7 +17582,7 @@ function credentialsRouter() {
|
|
|
16982
17582
|
}
|
|
16983
17583
|
}
|
|
16984
17584
|
try {
|
|
16985
|
-
reconcileAgentModels(wasActive ? { forcePrimary: true } : void 0);
|
|
17585
|
+
reconcileAgentModels(wasActive ? { forcePrimary: true, priorCarrier } : void 0);
|
|
16986
17586
|
} catch (e) {
|
|
16987
17587
|
process.stderr.write(`[credentials.delete] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
16988
17588
|
`);
|
|
@@ -17008,39 +17608,6 @@ var PROVIDERS = /* @__PURE__ */ new Set([
|
|
|
17008
17608
|
function isProvider(s) {
|
|
17009
17609
|
return PROVIDERS.has(s);
|
|
17010
17610
|
}
|
|
17011
|
-
function autoAssignVoicesOnFirstKey(provider) {
|
|
17012
|
-
if (provider !== "minimax" && provider !== "elevenlabs") return;
|
|
17013
|
-
const pool = listConfiguredVoices().filter((v) => v.provider === provider);
|
|
17014
|
-
if (pool.length === 0) return;
|
|
17015
|
-
const agents = listAllAgents();
|
|
17016
|
-
if (agents.length === 0) return;
|
|
17017
|
-
const shuffled = [...pool];
|
|
17018
|
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
17019
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
17020
|
-
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
17021
|
-
}
|
|
17022
|
-
for (let i = 0; i < agents.length; i++) {
|
|
17023
|
-
const v = shuffled[i % shuffled.length];
|
|
17024
|
-
const prev = agents[i].voice;
|
|
17025
|
-
const profile = {
|
|
17026
|
-
provider: v.provider,
|
|
17027
|
-
model: v.model,
|
|
17028
|
-
voiceId: v.voiceId,
|
|
17029
|
-
...prev?.speed !== void 0 ? { speed: prev.speed } : {},
|
|
17030
|
-
...prev?.pitch !== void 0 ? { pitch: prev.pitch } : {},
|
|
17031
|
-
...prev?.volume !== void 0 ? { volume: prev.volume } : {},
|
|
17032
|
-
...prev?.emotion !== void 0 ? { emotion: prev.emotion } : {}
|
|
17033
|
-
};
|
|
17034
|
-
try {
|
|
17035
|
-
updateAgent(agents[i].id, { voice: profile });
|
|
17036
|
-
} catch (e) {
|
|
17037
|
-
process.stderr.write(
|
|
17038
|
-
`[keys.put] auto-assign voice failed for ${agents[i].id}: ${e instanceof Error ? e.message : String(e)}
|
|
17039
|
-
`
|
|
17040
|
-
);
|
|
17041
|
-
}
|
|
17042
|
-
}
|
|
17043
|
-
}
|
|
17044
17611
|
function keysRouter() {
|
|
17045
17612
|
const r = new Hono5();
|
|
17046
17613
|
r.get("/", (c) => {
|
|
@@ -17066,6 +17633,12 @@ function keysRouter() {
|
|
|
17066
17633
|
if (isLlmProvider(provider)) {
|
|
17067
17634
|
return c.json({ error: "LLM providers use POST /api/credentials" }, 410);
|
|
17068
17635
|
}
|
|
17636
|
+
if (provider === "minimax" || provider === "elevenlabs") {
|
|
17637
|
+
return c.json({ error: "voice providers use POST /api/voice-credentials" }, 410);
|
|
17638
|
+
}
|
|
17639
|
+
if (provider === "brave" || provider === "tavily") {
|
|
17640
|
+
return c.json({ error: "search providers use POST /api/search-credentials" }, 410);
|
|
17641
|
+
}
|
|
17069
17642
|
let body;
|
|
17070
17643
|
try {
|
|
17071
17644
|
body = await c.req.json();
|
|
@@ -17074,16 +17647,7 @@ function keysRouter() {
|
|
|
17074
17647
|
}
|
|
17075
17648
|
const key = body?.key;
|
|
17076
17649
|
if (typeof key !== "string") return c.json({ error: "body must contain { key: string }" }, 400);
|
|
17077
|
-
const hadAnyVoiceKeyBefore = !!getKey("minimax") || !!getKey("elevenlabs");
|
|
17078
17650
|
setKey(provider, key);
|
|
17079
|
-
if ((provider === "minimax" || provider === "elevenlabs") && key.trim().length > 0 && !hadAnyVoiceKeyBefore) {
|
|
17080
|
-
try {
|
|
17081
|
-
autoAssignVoicesOnFirstKey(provider);
|
|
17082
|
-
} catch (e) {
|
|
17083
|
-
process.stderr.write(`[keys.put] voice auto-assign failed: ${e instanceof Error ? e.message : String(e)}
|
|
17084
|
-
`);
|
|
17085
|
-
}
|
|
17086
|
-
}
|
|
17087
17651
|
const fresh = listKeyMeta().find((m) => m.provider === provider);
|
|
17088
17652
|
return c.json(
|
|
17089
17653
|
fresh ?? { provider, configured: key.trim().length > 0, updatedAt: Date.now(), preview: null }
|
|
@@ -17095,6 +17659,12 @@ function keysRouter() {
|
|
|
17095
17659
|
if (isLlmProvider(provider)) {
|
|
17096
17660
|
return c.json({ error: "LLM providers use DELETE /api/credentials/:id" }, 410);
|
|
17097
17661
|
}
|
|
17662
|
+
if (provider === "minimax" || provider === "elevenlabs") {
|
|
17663
|
+
return c.json({ error: "voice providers use DELETE /api/voice-credentials/:id" }, 410);
|
|
17664
|
+
}
|
|
17665
|
+
if (provider === "brave" || provider === "tavily") {
|
|
17666
|
+
return c.json({ error: "search providers use DELETE /api/search-credentials/:id" }, 410);
|
|
17667
|
+
}
|
|
17098
17668
|
deleteKey(provider);
|
|
17099
17669
|
return c.json({ provider, configured: false, updatedAt: null, preview: null });
|
|
17100
17670
|
});
|
|
@@ -21899,6 +22469,7 @@ async function streamChairMessage(args) {
|
|
|
21899
22469
|
const metaKind = meta?.kind;
|
|
21900
22470
|
const isRoundEndVoice = voiceMode && metaKind === "round-end";
|
|
21901
22471
|
let pingDone = false;
|
|
22472
|
+
let tailDone = false;
|
|
21902
22473
|
let voiceBuf = "";
|
|
21903
22474
|
const voiceProfile = voiceMode ? voiceProfileForAgent(chair) : null;
|
|
21904
22475
|
let voiceSeq = 0;
|
|
@@ -21960,20 +22531,31 @@ async function streamChairMessage(args) {
|
|
|
21960
22531
|
});
|
|
21961
22532
|
if (voiceChunker) {
|
|
21962
22533
|
if (isRoundEndVoice) {
|
|
21963
|
-
|
|
21964
|
-
|
|
21965
|
-
|
|
21966
|
-
if (
|
|
21967
|
-
|
|
21968
|
-
|
|
21969
|
-
|
|
22534
|
+
if (tailDone) {
|
|
22535
|
+
} else {
|
|
22536
|
+
voiceBuf += chunk.delta;
|
|
22537
|
+
if (!pingDone) {
|
|
22538
|
+
const idx = voiceBuf.search(/POINTS\s*:/i);
|
|
22539
|
+
if (idx >= 0) {
|
|
22540
|
+
pingDone = true;
|
|
22541
|
+
const after = voiceBuf.slice(voiceBuf.search(/POINTS\s*:/i));
|
|
22542
|
+
voiceBuf = after.replace(/POINTS\s*:/i, "");
|
|
22543
|
+
}
|
|
21970
22544
|
}
|
|
21971
|
-
|
|
21972
|
-
|
|
21973
|
-
|
|
21974
|
-
|
|
21975
|
-
|
|
21976
|
-
|
|
22545
|
+
if (pingDone && voiceBuf) {
|
|
22546
|
+
const tailIdx = voiceBuf.search(/MODE[-\s]?SHIFT\s*:/i);
|
|
22547
|
+
if (tailIdx >= 0) {
|
|
22548
|
+
tailDone = true;
|
|
22549
|
+
const beforeTail = voiceBuf.slice(0, tailIdx);
|
|
22550
|
+
voiceBuf = beforeTail;
|
|
22551
|
+
}
|
|
22552
|
+
if (voiceBuf) {
|
|
22553
|
+
const cleaned = voiceBuf.replace(/(^|\n)\s*[-*]\s*/g, ". ").replace(/\bBECAUSE\s*:\s*/gi, "");
|
|
22554
|
+
voiceBuf = "";
|
|
22555
|
+
for (const spoken of voiceChunker.push(cleaned)) {
|
|
22556
|
+
await emitChairVoice(spoken);
|
|
22557
|
+
}
|
|
22558
|
+
}
|
|
21977
22559
|
}
|
|
21978
22560
|
}
|
|
21979
22561
|
} else {
|
|
@@ -23114,6 +23696,129 @@ async function pickDirectors(opts) {
|
|
|
23114
23696
|
};
|
|
23115
23697
|
}
|
|
23116
23698
|
|
|
23699
|
+
// src/orchestrator/roomTitle.ts
|
|
23700
|
+
var MAX_TITLE_CHARS = 32;
|
|
23701
|
+
var REJECT_PHRASES = /* @__PURE__ */ new Set([
|
|
23702
|
+
"untitled",
|
|
23703
|
+
"untitled room",
|
|
23704
|
+
"discussion",
|
|
23705
|
+
"chat",
|
|
23706
|
+
"conversation",
|
|
23707
|
+
"topic",
|
|
23708
|
+
"summary",
|
|
23709
|
+
"\u672A\u547D\u540D",
|
|
23710
|
+
"\u8BA8\u8BBA",
|
|
23711
|
+
"\u5BF9\u8BDD",
|
|
23712
|
+
"\u804A\u5929"
|
|
23713
|
+
]);
|
|
23714
|
+
async function generateRoomTitle(roomId) {
|
|
23715
|
+
const room = getRoom(roomId);
|
|
23716
|
+
if (!room) return { kind: "skipped", reason: "no-room" };
|
|
23717
|
+
if (!room.nameAuto) return { kind: "skipped", reason: "user-named" };
|
|
23718
|
+
const subject = room.subject.trim();
|
|
23719
|
+
if (!subject) return { kind: "skipped", reason: "no-subject" };
|
|
23720
|
+
const fallbackName = room.subject.slice(0, 60);
|
|
23721
|
+
if (room.name !== fallbackName) {
|
|
23722
|
+
return { kind: "skipped", reason: "already-renamed", detail: room.name.slice(0, 60) };
|
|
23723
|
+
}
|
|
23724
|
+
const modelV = utilityModelFor();
|
|
23725
|
+
if (!modelV) return { kind: "skipped", reason: "no-model" };
|
|
23726
|
+
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.
|
|
23727
|
+
|
|
23728
|
+
How to write a representative title:
|
|
23729
|
+
1. Identify the CORE SUBJECT or TASK (the noun, the deliverable, the decision being made).
|
|
23730
|
+
2. Strip throat-clearing, polite framing, self-introduction, and product names that are not the subject itself.
|
|
23731
|
+
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").
|
|
23732
|
+
4. Use the SAME language as the opening question.
|
|
23733
|
+
|
|
23734
|
+
Length:
|
|
23735
|
+
- Chinese / Japanese: 5-10 characters.
|
|
23736
|
+
- English / Spanish / other Latin scripts: 3-6 words.
|
|
23737
|
+
- \u226424 characters total.
|
|
23738
|
+
|
|
23739
|
+
Format:
|
|
23740
|
+
- Output ONLY the title \u2014 no quotes, no brackets, no trailing punctuation, no labels like "Topic:" / "\u4E3B\u9898\uFF1A", no explanation.
|
|
23741
|
+
- Never output fillers like "Untitled", "Discussion", "Chat", "Conversation", "\u8BA8\u8BBA", "\u5BF9\u8BDD", "\u804A\u5929".
|
|
23742
|
+
|
|
23743
|
+
Examples:
|
|
23744
|
+
|
|
23745
|
+
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
|
|
23746
|
+
Output: \u4EA7\u54C1\u5BA3\u4F20\u89C6\u9891\u811A\u672C
|
|
23747
|
+
|
|
23748
|
+
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
|
|
23749
|
+
Output: Postgres \u8F6C ClickHouse \u6743\u8861
|
|
23750
|
+
|
|
23751
|
+
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
|
|
23752
|
+
Output: LoRA vs \u5168\u91CF\u5FAE\u8C03
|
|
23753
|
+
|
|
23754
|
+
Input: Can you help me debug this Python regex that's failing on Unicode strings with combining marks?
|
|
23755
|
+
Output: Python regex Unicode bug
|
|
23756
|
+
|
|
23757
|
+
Input: I want to redesign our onboarding email sequence \u2014 currently 5 emails over 2 weeks, low click-through.
|
|
23758
|
+
Output: Onboarding email redesign
|
|
23759
|
+
|
|
23760
|
+
--- User's opening question ---
|
|
23761
|
+
${subject}
|
|
23762
|
+
|
|
23763
|
+
--- Title ---
|
|
23764
|
+
`;
|
|
23765
|
+
let raw = "";
|
|
23766
|
+
try {
|
|
23767
|
+
raw = await callLLM({
|
|
23768
|
+
modelV,
|
|
23769
|
+
carrier: null,
|
|
23770
|
+
messages: [{ role: "user", content: prompt }],
|
|
23771
|
+
// Low but not zero · 0.2 was deterministic-ish but kept locking
|
|
23772
|
+
// onto a generic first-noun pick. 0.4 lets the model trade off
|
|
23773
|
+
// alternatives without wandering into creative territory.
|
|
23774
|
+
temperature: 0.4,
|
|
23775
|
+
// 40 was tight enough that a model thinking briefly before
|
|
23776
|
+
// answering would get cut off mid-title; 80 fits the title plus
|
|
23777
|
+
// a small margin without inviting paragraphs.
|
|
23778
|
+
maxTokens: 80
|
|
23779
|
+
});
|
|
23780
|
+
} catch (e) {
|
|
23781
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
23782
|
+
process.stderr.write(`[room-title] LLM call failed for ${roomId}: ${detail}
|
|
23783
|
+
`);
|
|
23784
|
+
return { kind: "skipped", reason: "llm-error", detail };
|
|
23785
|
+
}
|
|
23786
|
+
if (!raw.trim()) {
|
|
23787
|
+
return { kind: "skipped", reason: "empty-output", detail: `model=${modelV}` };
|
|
23788
|
+
}
|
|
23789
|
+
const phrase = sanitiseTitle(raw);
|
|
23790
|
+
if (!phrase) {
|
|
23791
|
+
return { kind: "skipped", reason: "rejected-generic", detail: raw.trim().slice(0, 80) };
|
|
23792
|
+
}
|
|
23793
|
+
const updated = setRoomNameFromAuto(roomId, phrase);
|
|
23794
|
+
if (!updated) return { kind: "skipped", reason: "race-after-rename" };
|
|
23795
|
+
roomBus.emit(roomId, {
|
|
23796
|
+
type: "config-event",
|
|
23797
|
+
kind: "settings-changed",
|
|
23798
|
+
payload: { changes: { name: { from: room.name, to: phrase } } },
|
|
23799
|
+
createdAt: Date.now()
|
|
23800
|
+
});
|
|
23801
|
+
return { kind: "ok", before: room.name, after: phrase };
|
|
23802
|
+
}
|
|
23803
|
+
function sanitiseTitle(raw) {
|
|
23804
|
+
let s = raw.trim();
|
|
23805
|
+
if (!s) return null;
|
|
23806
|
+
const firstLine = s.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
|
|
23807
|
+
if (firstLine) s = firstLine;
|
|
23808
|
+
s = s.replace(/^\s*output\s*[::]\s*/i, "");
|
|
23809
|
+
s = s.replace(/^\s*(topic|title|主题|主題|标题|標題|タイトル)\s*[::]\s*/i, "");
|
|
23810
|
+
s = s.replace(/^[\s"'`「『《【〈“‘]+/, "").replace(/[\s"'`」』》】〉”’]+$/, "");
|
|
23811
|
+
s = s.replace(/[\s.。!!??,,;;::]+$/, "");
|
|
23812
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
23813
|
+
if (!s) return null;
|
|
23814
|
+
if (REJECT_PHRASES.has(s.toLowerCase())) return null;
|
|
23815
|
+
const cps = Array.from(s);
|
|
23816
|
+
if (cps.length > MAX_TITLE_CHARS) {
|
|
23817
|
+
s = cps.slice(0, MAX_TITLE_CHARS).join("");
|
|
23818
|
+
}
|
|
23819
|
+
return s;
|
|
23820
|
+
}
|
|
23821
|
+
|
|
23117
23822
|
// src/routes/rooms.ts
|
|
23118
23823
|
async function runAutoPickAndSeat(roomId, subject) {
|
|
23119
23824
|
const candidates = listAgents().filter((a) => a.roleKind === "director");
|
|
@@ -23233,7 +23938,10 @@ function roomsRouter() {
|
|
|
23233
23938
|
);
|
|
23234
23939
|
}
|
|
23235
23940
|
}
|
|
23236
|
-
const
|
|
23941
|
+
const rawName = typeof b.name === "string" ? b.name.trim() : "";
|
|
23942
|
+
const hasExplicitName = rawName.length > 0;
|
|
23943
|
+
const name = hasExplicitName ? rawName.slice(0, 80) : subject.slice(0, 60);
|
|
23944
|
+
const nameAuto = !hasExplicitName;
|
|
23237
23945
|
const ALLOWED_MODES = /* @__PURE__ */ new Set(["brainstorm", "constructive", "research", "debate", "critique"]);
|
|
23238
23946
|
const ALLOWED_INTENSITY = /* @__PURE__ */ new Set(["calm", "sharp", "terse", "brutal"]);
|
|
23239
23947
|
const normalizeIntensityValue = (v) => v === "brutal" ? "terse" : v;
|
|
@@ -23256,7 +23964,8 @@ function roomsRouter() {
|
|
|
23256
23964
|
deliveryMode,
|
|
23257
23965
|
agentIds,
|
|
23258
23966
|
parentRoomId,
|
|
23259
|
-
parentBriefId
|
|
23967
|
+
parentBriefId,
|
|
23968
|
+
nameAuto
|
|
23260
23969
|
});
|
|
23261
23970
|
insertConfigEvent({
|
|
23262
23971
|
roomId: room.id,
|
|
@@ -23282,6 +23991,25 @@ function roomsRouter() {
|
|
|
23282
23991
|
createdAt: opening.createdAt
|
|
23283
23992
|
});
|
|
23284
23993
|
roomBus.emit(room.id, { type: "message-final", messageId: opening.id });
|
|
23994
|
+
generateRoomTitle(room.id).then((result) => {
|
|
23995
|
+
if (result.kind === "ok") {
|
|
23996
|
+
process.stderr.write(
|
|
23997
|
+
`[room-title] room=${room.id} renamed "${result.before.slice(0, 40)}" \u2192 "${result.after}"
|
|
23998
|
+
`
|
|
23999
|
+
);
|
|
24000
|
+
} else {
|
|
24001
|
+
const tail = result.detail ? ` \xB7 detail=${result.detail.slice(0, 100)}` : "";
|
|
24002
|
+
process.stderr.write(
|
|
24003
|
+
`[room-title] room=${room.id} skipped \xB7 reason=${result.reason}${tail}
|
|
24004
|
+
`
|
|
24005
|
+
);
|
|
24006
|
+
}
|
|
24007
|
+
}).catch((e) => {
|
|
24008
|
+
process.stderr.write(
|
|
24009
|
+
`[room-title] room=${room.id} threw: ${e instanceof Error ? e.message : String(e)}
|
|
24010
|
+
`
|
|
24011
|
+
);
|
|
24012
|
+
});
|
|
23285
24013
|
setAwaitingClarify(room.id, true);
|
|
23286
24014
|
if (mode === "research" && !hasWebSearchKey()) {
|
|
23287
24015
|
const langGuess = /[一-鿿]/.test(subject) ? "zh" : "en";
|
|
@@ -24124,10 +24852,122 @@ function buildRoomExportMarkdown(opts) {
|
|
|
24124
24852
|
return [...headerLines, ...transcriptLines, ...briefsLines].join("\n") + "\n";
|
|
24125
24853
|
}
|
|
24126
24854
|
|
|
24127
|
-
// src/routes/search.ts
|
|
24855
|
+
// src/routes/search-credentials.ts
|
|
24128
24856
|
import { Hono as Hono10 } from "hono";
|
|
24129
|
-
function
|
|
24857
|
+
function payloadFor2(meta, activeId) {
|
|
24858
|
+
return {
|
|
24859
|
+
id: meta.id,
|
|
24860
|
+
provider: meta.provider,
|
|
24861
|
+
label: meta.label,
|
|
24862
|
+
preview: meta.preview,
|
|
24863
|
+
createdAt: meta.createdAt,
|
|
24864
|
+
updatedAt: meta.updatedAt,
|
|
24865
|
+
isActive: meta.id === activeId
|
|
24866
|
+
};
|
|
24867
|
+
}
|
|
24868
|
+
function pickNextActiveSearchId(removedProvider) {
|
|
24869
|
+
const all = listSearchCredentials();
|
|
24870
|
+
if (all.length === 0) return null;
|
|
24871
|
+
if (removedProvider) {
|
|
24872
|
+
const sameProvider = all.filter((c) => c.provider === removedProvider);
|
|
24873
|
+
if (sameProvider.length > 0) {
|
|
24874
|
+
sameProvider.sort((a, b) => a.createdAt - b.createdAt);
|
|
24875
|
+
return sameProvider[0].id;
|
|
24876
|
+
}
|
|
24877
|
+
}
|
|
24878
|
+
const sorted = all.slice().sort((a, b) => {
|
|
24879
|
+
const ai = SEARCH_PROVIDER_PRIORITY.indexOf(a.provider);
|
|
24880
|
+
const bi = SEARCH_PROVIDER_PRIORITY.indexOf(b.provider);
|
|
24881
|
+
if (ai !== bi) return ai - bi;
|
|
24882
|
+
return a.createdAt - b.createdAt;
|
|
24883
|
+
});
|
|
24884
|
+
return sorted[0]?.id ?? null;
|
|
24885
|
+
}
|
|
24886
|
+
function searchCredentialsRouter() {
|
|
24130
24887
|
const r = new Hono10();
|
|
24888
|
+
r.get("/", (c) => {
|
|
24889
|
+
const activeId = getPrefs().activeSearchCredentialId;
|
|
24890
|
+
const items = listSearchCredentials().map((m) => payloadFor2(m, activeId));
|
|
24891
|
+
return c.json({
|
|
24892
|
+
credentials: items,
|
|
24893
|
+
activeId
|
|
24894
|
+
});
|
|
24895
|
+
});
|
|
24896
|
+
r.put("/active", async (c) => {
|
|
24897
|
+
let body;
|
|
24898
|
+
try {
|
|
24899
|
+
body = await c.req.json();
|
|
24900
|
+
} catch {
|
|
24901
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
24902
|
+
}
|
|
24903
|
+
const rawId = body?.id;
|
|
24904
|
+
let nextId;
|
|
24905
|
+
if (rawId === null || rawId === void 0) {
|
|
24906
|
+
nextId = null;
|
|
24907
|
+
} else if (typeof rawId === "string") {
|
|
24908
|
+
nextId = rawId;
|
|
24909
|
+
} else {
|
|
24910
|
+
return c.json({ error: "id must be a string or null" }, 400);
|
|
24911
|
+
}
|
|
24912
|
+
if (nextId) {
|
|
24913
|
+
const meta = getSearchCredentialMeta(nextId);
|
|
24914
|
+
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
24915
|
+
updatePrefs({ activeSearchCredentialId: nextId });
|
|
24916
|
+
} else {
|
|
24917
|
+
updatePrefs({ activeSearchCredentialId: null });
|
|
24918
|
+
}
|
|
24919
|
+
return c.json({ activeId: nextId });
|
|
24920
|
+
});
|
|
24921
|
+
r.post("/", async (c) => {
|
|
24922
|
+
let body;
|
|
24923
|
+
try {
|
|
24924
|
+
body = await c.req.json();
|
|
24925
|
+
} catch {
|
|
24926
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
24927
|
+
}
|
|
24928
|
+
const provider = body?.provider;
|
|
24929
|
+
const labelRaw = body?.label;
|
|
24930
|
+
const key = body?.key;
|
|
24931
|
+
if (typeof provider !== "string" || !isSearchProvider(provider)) {
|
|
24932
|
+
return c.json({ error: "provider must be 'brave' or 'tavily'" }, 400);
|
|
24933
|
+
}
|
|
24934
|
+
if (typeof key !== "string" || key.trim().length === 0) {
|
|
24935
|
+
return c.json({ error: "key must be a non-empty string" }, 400);
|
|
24936
|
+
}
|
|
24937
|
+
const label = typeof labelRaw === "string" ? labelRaw : null;
|
|
24938
|
+
const meta = createSearchCredential(provider, label, key);
|
|
24939
|
+
if (!meta) return c.json({ error: "failed to create credential" }, 500);
|
|
24940
|
+
const hadActive = !!getPrefs().activeSearchCredentialId;
|
|
24941
|
+
if (!hadActive) {
|
|
24942
|
+
updatePrefs({ activeSearchCredentialId: meta.id });
|
|
24943
|
+
}
|
|
24944
|
+
const activeId = getPrefs().activeSearchCredentialId;
|
|
24945
|
+
return c.json(payloadFor2(meta, activeId), 201);
|
|
24946
|
+
});
|
|
24947
|
+
r.delete("/:id", (c) => {
|
|
24948
|
+
const id = c.req.param("id");
|
|
24949
|
+
const meta = getSearchCredentialMeta(id);
|
|
24950
|
+
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
24951
|
+
const prefs = getPrefs();
|
|
24952
|
+
const wasActive = prefs.activeSearchCredentialId === id;
|
|
24953
|
+
const removedProvider = deleteSearchCredential(id);
|
|
24954
|
+
if (wasActive) {
|
|
24955
|
+
const nextId = pickNextActiveSearchId(removedProvider);
|
|
24956
|
+
updatePrefs({ activeSearchCredentialId: nextId });
|
|
24957
|
+
}
|
|
24958
|
+
return c.json({
|
|
24959
|
+
id,
|
|
24960
|
+
deleted: true,
|
|
24961
|
+
activeId: getPrefs().activeSearchCredentialId
|
|
24962
|
+
});
|
|
24963
|
+
});
|
|
24964
|
+
return r;
|
|
24965
|
+
}
|
|
24966
|
+
|
|
24967
|
+
// src/routes/search.ts
|
|
24968
|
+
import { Hono as Hono11 } from "hono";
|
|
24969
|
+
function searchRouter() {
|
|
24970
|
+
const r = new Hono11();
|
|
24131
24971
|
r.get("/", (c) => {
|
|
24132
24972
|
const q = (c.req.query("q") || "").trim();
|
|
24133
24973
|
if (q.length < 1) {
|
|
@@ -24166,7 +25006,7 @@ function searchRouter() {
|
|
|
24166
25006
|
}
|
|
24167
25007
|
|
|
24168
25008
|
// src/routes/usage.ts
|
|
24169
|
-
import { Hono as
|
|
25009
|
+
import { Hono as Hono12 } from "hono";
|
|
24170
25010
|
function modelDisplay(modelV) {
|
|
24171
25011
|
if (isModelV(modelV)) {
|
|
24172
25012
|
const m = MODELS[modelV];
|
|
@@ -24175,7 +25015,7 @@ function modelDisplay(modelV) {
|
|
|
24175
25015
|
return { displayName: modelV, provider: "unknown" };
|
|
24176
25016
|
}
|
|
24177
25017
|
function usageRouter() {
|
|
24178
|
-
const r = new
|
|
25018
|
+
const r = new Hono12();
|
|
24179
25019
|
r.get("/summary", (c) => {
|
|
24180
25020
|
const s = getUsageSummary();
|
|
24181
25021
|
return c.json({
|
|
@@ -24224,8 +25064,293 @@ function usageRouter() {
|
|
|
24224
25064
|
return r;
|
|
24225
25065
|
}
|
|
24226
25066
|
|
|
25067
|
+
// src/routes/voice-credentials.ts
|
|
25068
|
+
import { Hono as Hono13 } from "hono";
|
|
25069
|
+
|
|
25070
|
+
// src/storage/reconcile-voices.ts
|
|
25071
|
+
var MINIMAX_SEED_VOICES = [
|
|
25072
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "male-qn-qingse" },
|
|
25073
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "female-shaonv" },
|
|
25074
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "female-yujie" },
|
|
25075
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "male-qn-jingying" },
|
|
25076
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "female-chengshu" },
|
|
25077
|
+
{ provider: "minimax", model: "speech-2.8-hd", voiceId: "female-tianmei" }
|
|
25078
|
+
];
|
|
25079
|
+
var ELEVENLABS_SEED_VOICES = [
|
|
25080
|
+
{ provider: "elevenlabs", model: "eleven_multilingual_v2", voiceId: "21m00Tcm4TlvDq8ikWAM" },
|
|
25081
|
+
{ provider: "elevenlabs", model: "eleven_multilingual_v2", voiceId: "JBFqnCBsd6RMkjVDRZzb" }
|
|
25082
|
+
];
|
|
25083
|
+
function shuffle(arr) {
|
|
25084
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
25085
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
25086
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
25087
|
+
}
|
|
25088
|
+
return arr;
|
|
25089
|
+
}
|
|
25090
|
+
function snapshotPrior(agent, priorProvider, targetProvider) {
|
|
25091
|
+
if (!priorProvider) return;
|
|
25092
|
+
if (priorProvider === targetProvider) return;
|
|
25093
|
+
if (!agent.voice) return;
|
|
25094
|
+
if (agent.voice.provider !== priorProvider) return;
|
|
25095
|
+
try {
|
|
25096
|
+
writeVoiceBucketEntry(agent.id, priorProvider, agent.voice);
|
|
25097
|
+
} catch (e) {
|
|
25098
|
+
process.stderr.write(
|
|
25099
|
+
`[reconcile-voices] snapshot failed for ${agent.id}: ${e instanceof Error ? e.message : String(e)}
|
|
25100
|
+
`
|
|
25101
|
+
);
|
|
25102
|
+
}
|
|
25103
|
+
}
|
|
25104
|
+
function reconcileAgentVoices(opts) {
|
|
25105
|
+
const targetProvider = getActiveVoiceProvider();
|
|
25106
|
+
const priorProvider = opts.priorProvider ?? null;
|
|
25107
|
+
const agents = listAllAgents();
|
|
25108
|
+
if (agents.length === 0) {
|
|
25109
|
+
return { changed: 0, cleared: 0, reason: opts.reason, toProvider: targetProvider };
|
|
25110
|
+
}
|
|
25111
|
+
if (!targetProvider) {
|
|
25112
|
+
let cleared = 0;
|
|
25113
|
+
for (const a of agents) {
|
|
25114
|
+
snapshotPrior(a, priorProvider, null);
|
|
25115
|
+
if (a.voice) {
|
|
25116
|
+
try {
|
|
25117
|
+
updateAgent(a.id, { voice: null });
|
|
25118
|
+
cleared++;
|
|
25119
|
+
} catch (e) {
|
|
25120
|
+
process.stderr.write(
|
|
25121
|
+
`[reconcile-voices] clear failed for ${a.id}: ${e instanceof Error ? e.message : String(e)}
|
|
25122
|
+
`
|
|
25123
|
+
);
|
|
25124
|
+
}
|
|
25125
|
+
}
|
|
25126
|
+
}
|
|
25127
|
+
process.stderr.write(`[reconcile-voices] reason=${opts.reason} toProvider=null cleared=${cleared}
|
|
25128
|
+
`);
|
|
25129
|
+
return { changed: 0, cleared, reason: opts.reason, toProvider: null };
|
|
25130
|
+
}
|
|
25131
|
+
const pool = targetProvider === "minimax" ? MINIMAX_SEED_VOICES : targetProvider === "elevenlabs" ? ELEVENLABS_SEED_VOICES : [];
|
|
25132
|
+
if (pool.length === 0) {
|
|
25133
|
+
process.stderr.write(`[reconcile-voices] reason=${opts.reason} toProvider=${targetProvider} no-pool \xB7 skipped
|
|
25134
|
+
`);
|
|
25135
|
+
return { changed: 0, cleared: 0, reason: opts.reason, toProvider: targetProvider };
|
|
25136
|
+
}
|
|
25137
|
+
const shuffled = shuffle([...pool]);
|
|
25138
|
+
let changed = 0;
|
|
25139
|
+
const targetVp = targetProvider;
|
|
25140
|
+
for (let i = 0; i < agents.length; i++) {
|
|
25141
|
+
const a = agents[i];
|
|
25142
|
+
snapshotPrior(a, priorProvider, targetVp);
|
|
25143
|
+
const bucket = getVoiceBucket(a.id);
|
|
25144
|
+
const memorised = bucket[targetVp];
|
|
25145
|
+
const prev = a.voice;
|
|
25146
|
+
let profile;
|
|
25147
|
+
if (memorised) {
|
|
25148
|
+
profile = {
|
|
25149
|
+
provider: memorised.provider,
|
|
25150
|
+
model: memorised.model,
|
|
25151
|
+
voiceId: memorised.voiceId,
|
|
25152
|
+
...prev?.speed !== void 0 ? { speed: prev.speed } : memorised.speed !== void 0 ? { speed: memorised.speed } : {},
|
|
25153
|
+
...prev?.pitch !== void 0 ? { pitch: prev.pitch } : memorised.pitch !== void 0 ? { pitch: memorised.pitch } : {},
|
|
25154
|
+
...prev?.volume !== void 0 ? { volume: prev.volume } : memorised.volume !== void 0 ? { volume: memorised.volume } : {},
|
|
25155
|
+
...prev?.emotion !== void 0 ? { emotion: prev.emotion } : memorised.emotion !== void 0 ? { emotion: memorised.emotion } : {},
|
|
25156
|
+
...memorised.modifyPitch !== void 0 ? { modifyPitch: memorised.modifyPitch } : {},
|
|
25157
|
+
...memorised.modifyIntensity !== void 0 ? { modifyIntensity: memorised.modifyIntensity } : {},
|
|
25158
|
+
...memorised.modifyTimbre !== void 0 ? { modifyTimbre: memorised.modifyTimbre } : {},
|
|
25159
|
+
...memorised.instructions !== void 0 ? { instructions: memorised.instructions } : {}
|
|
25160
|
+
};
|
|
25161
|
+
} else {
|
|
25162
|
+
if (opts.reason === "first-key" && a.voice && a.voice.provider === targetVp) {
|
|
25163
|
+
if (!bucket[targetVp]) {
|
|
25164
|
+
try {
|
|
25165
|
+
writeVoiceBucketEntry(a.id, targetVp, a.voice);
|
|
25166
|
+
} catch (e) {
|
|
25167
|
+
process.stderr.write(
|
|
25168
|
+
`[reconcile-voices] seed failed for ${a.id}: ${e instanceof Error ? e.message : String(e)}
|
|
25169
|
+
`
|
|
25170
|
+
);
|
|
25171
|
+
}
|
|
25172
|
+
}
|
|
25173
|
+
continue;
|
|
25174
|
+
}
|
|
25175
|
+
const pick = shuffled[i % shuffled.length];
|
|
25176
|
+
profile = {
|
|
25177
|
+
provider: pick.provider,
|
|
25178
|
+
model: pick.model,
|
|
25179
|
+
voiceId: pick.voiceId,
|
|
25180
|
+
...prev?.speed !== void 0 ? { speed: prev.speed } : {},
|
|
25181
|
+
...prev?.pitch !== void 0 ? { pitch: prev.pitch } : {},
|
|
25182
|
+
...prev?.volume !== void 0 ? { volume: prev.volume } : {},
|
|
25183
|
+
...prev?.emotion !== void 0 ? { emotion: prev.emotion } : {}
|
|
25184
|
+
};
|
|
25185
|
+
}
|
|
25186
|
+
try {
|
|
25187
|
+
updateAgent(a.id, { voice: profile });
|
|
25188
|
+
writeVoiceBucketEntry(a.id, targetVp, profile);
|
|
25189
|
+
changed++;
|
|
25190
|
+
} catch (e) {
|
|
25191
|
+
process.stderr.write(
|
|
25192
|
+
`[reconcile-voices] update failed for ${a.id}: ${e instanceof Error ? e.message : String(e)}
|
|
25193
|
+
`
|
|
25194
|
+
);
|
|
25195
|
+
}
|
|
25196
|
+
}
|
|
25197
|
+
process.stderr.write(
|
|
25198
|
+
`[reconcile-voices] reason=${opts.reason} toProvider=${targetProvider} changed=${changed}
|
|
25199
|
+
`
|
|
25200
|
+
);
|
|
25201
|
+
return { changed, cleared: 0, reason: opts.reason, toProvider: targetProvider };
|
|
25202
|
+
}
|
|
25203
|
+
|
|
25204
|
+
// src/routes/voice-credentials.ts
|
|
25205
|
+
function payloadFor3(meta, activeId) {
|
|
25206
|
+
return {
|
|
25207
|
+
id: meta.id,
|
|
25208
|
+
provider: meta.provider,
|
|
25209
|
+
label: meta.label,
|
|
25210
|
+
preview: meta.preview,
|
|
25211
|
+
createdAt: meta.createdAt,
|
|
25212
|
+
updatedAt: meta.updatedAt,
|
|
25213
|
+
isActive: meta.id === activeId
|
|
25214
|
+
};
|
|
25215
|
+
}
|
|
25216
|
+
function pickNextActiveVoiceId(removedProvider) {
|
|
25217
|
+
const all = listVoiceCredentials();
|
|
25218
|
+
if (all.length === 0) return null;
|
|
25219
|
+
if (removedProvider) {
|
|
25220
|
+
const sameProvider = all.filter((c) => c.provider === removedProvider);
|
|
25221
|
+
if (sameProvider.length > 0) {
|
|
25222
|
+
sameProvider.sort((a, b) => a.createdAt - b.createdAt);
|
|
25223
|
+
return sameProvider[0].id;
|
|
25224
|
+
}
|
|
25225
|
+
}
|
|
25226
|
+
const sorted = all.slice().sort((a, b) => {
|
|
25227
|
+
const ai = VOICE_PROVIDER_PRIORITY.indexOf(a.provider);
|
|
25228
|
+
const bi = VOICE_PROVIDER_PRIORITY.indexOf(b.provider);
|
|
25229
|
+
if (ai !== bi) return ai - bi;
|
|
25230
|
+
return a.createdAt - b.createdAt;
|
|
25231
|
+
});
|
|
25232
|
+
return sorted[0]?.id ?? null;
|
|
25233
|
+
}
|
|
25234
|
+
function voiceCredentialsRouter() {
|
|
25235
|
+
const r = new Hono13();
|
|
25236
|
+
r.get("/", (c) => {
|
|
25237
|
+
const activeId = getPrefs().activeVoiceCredentialId;
|
|
25238
|
+
const items = listVoiceCredentials().map((m) => payloadFor3(m, activeId));
|
|
25239
|
+
return c.json({
|
|
25240
|
+
credentials: items,
|
|
25241
|
+
activeId
|
|
25242
|
+
});
|
|
25243
|
+
});
|
|
25244
|
+
r.put("/active", async (c) => {
|
|
25245
|
+
let body;
|
|
25246
|
+
try {
|
|
25247
|
+
body = await c.req.json();
|
|
25248
|
+
} catch {
|
|
25249
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
25250
|
+
}
|
|
25251
|
+
const rawId = body?.id;
|
|
25252
|
+
let nextId;
|
|
25253
|
+
if (rawId === null || rawId === void 0) {
|
|
25254
|
+
nextId = null;
|
|
25255
|
+
} else if (typeof rawId === "string") {
|
|
25256
|
+
nextId = rawId;
|
|
25257
|
+
} else {
|
|
25258
|
+
return c.json({ error: "id must be a string or null" }, 400);
|
|
25259
|
+
}
|
|
25260
|
+
const prefs = getPrefs();
|
|
25261
|
+
const priorActiveId = prefs.activeVoiceCredentialId;
|
|
25262
|
+
const priorProvider = priorActiveId ? getVoiceCredentialMeta(priorActiveId)?.provider ?? null : null;
|
|
25263
|
+
let nextProvider = null;
|
|
25264
|
+
if (nextId) {
|
|
25265
|
+
const meta = getVoiceCredentialMeta(nextId);
|
|
25266
|
+
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
25267
|
+
nextProvider = meta.provider;
|
|
25268
|
+
updatePrefs({ activeVoiceCredentialId: nextId });
|
|
25269
|
+
} else {
|
|
25270
|
+
updatePrefs({ activeVoiceCredentialId: null });
|
|
25271
|
+
}
|
|
25272
|
+
if (priorProvider !== nextProvider) {
|
|
25273
|
+
try {
|
|
25274
|
+
reconcileAgentVoices({ reason: "provider-switch", priorProvider });
|
|
25275
|
+
} catch (e) {
|
|
25276
|
+
process.stderr.write(
|
|
25277
|
+
`[voice-credentials.active] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
25278
|
+
`
|
|
25279
|
+
);
|
|
25280
|
+
}
|
|
25281
|
+
}
|
|
25282
|
+
return c.json({ activeId: nextId });
|
|
25283
|
+
});
|
|
25284
|
+
r.post("/", async (c) => {
|
|
25285
|
+
let body;
|
|
25286
|
+
try {
|
|
25287
|
+
body = await c.req.json();
|
|
25288
|
+
} catch {
|
|
25289
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
25290
|
+
}
|
|
25291
|
+
const provider = body?.provider;
|
|
25292
|
+
const labelRaw = body?.label;
|
|
25293
|
+
const key = body?.key;
|
|
25294
|
+
if (typeof provider !== "string" || !isVoiceProvider(provider)) {
|
|
25295
|
+
return c.json({ error: "provider must be 'minimax' or 'elevenlabs'" }, 400);
|
|
25296
|
+
}
|
|
25297
|
+
if (typeof key !== "string" || key.trim().length === 0) {
|
|
25298
|
+
return c.json({ error: "key must be a non-empty string" }, 400);
|
|
25299
|
+
}
|
|
25300
|
+
const label = typeof labelRaw === "string" ? labelRaw : null;
|
|
25301
|
+
const meta = createVoiceCredential(provider, label, key);
|
|
25302
|
+
if (!meta) return c.json({ error: "failed to create credential" }, 500);
|
|
25303
|
+
const hadActive = !!getPrefs().activeVoiceCredentialId;
|
|
25304
|
+
if (!hadActive) {
|
|
25305
|
+
updatePrefs({ activeVoiceCredentialId: meta.id });
|
|
25306
|
+
try {
|
|
25307
|
+
reconcileAgentVoices({ reason: "first-key", priorProvider: null });
|
|
25308
|
+
} catch (e) {
|
|
25309
|
+
process.stderr.write(
|
|
25310
|
+
`[voice-credentials.post] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
25311
|
+
`
|
|
25312
|
+
);
|
|
25313
|
+
}
|
|
25314
|
+
}
|
|
25315
|
+
const activeId = getPrefs().activeVoiceCredentialId;
|
|
25316
|
+
return c.json(payloadFor3(meta, activeId), 201);
|
|
25317
|
+
});
|
|
25318
|
+
r.delete("/:id", (c) => {
|
|
25319
|
+
const id = c.req.param("id");
|
|
25320
|
+
const meta = getVoiceCredentialMeta(id);
|
|
25321
|
+
if (!meta) return c.json({ error: "credential not found" }, 404);
|
|
25322
|
+
const prefs = getPrefs();
|
|
25323
|
+
const wasActive = prefs.activeVoiceCredentialId === id;
|
|
25324
|
+
const removedProvider = deleteVoiceCredential(id);
|
|
25325
|
+
let reshuffled = false;
|
|
25326
|
+
if (wasActive) {
|
|
25327
|
+
const nextId = pickNextActiveVoiceId(removedProvider);
|
|
25328
|
+
updatePrefs({ activeVoiceCredentialId: nextId });
|
|
25329
|
+
const nextProvider = nextId ? getVoiceCredentialMeta(nextId)?.provider ?? null : null;
|
|
25330
|
+
if (nextProvider !== removedProvider) {
|
|
25331
|
+
try {
|
|
25332
|
+
reconcileAgentVoices({ reason: "provider-switch", priorProvider: removedProvider });
|
|
25333
|
+
reshuffled = true;
|
|
25334
|
+
} catch (e) {
|
|
25335
|
+
process.stderr.write(
|
|
25336
|
+
`[voice-credentials.delete] reconcile failed: ${e instanceof Error ? e.message : String(e)}
|
|
25337
|
+
`
|
|
25338
|
+
);
|
|
25339
|
+
}
|
|
25340
|
+
}
|
|
25341
|
+
}
|
|
25342
|
+
return c.json({
|
|
25343
|
+
id,
|
|
25344
|
+
deleted: true,
|
|
25345
|
+
activeId: getPrefs().activeVoiceCredentialId,
|
|
25346
|
+
reshuffled
|
|
25347
|
+
});
|
|
25348
|
+
});
|
|
25349
|
+
return r;
|
|
25350
|
+
}
|
|
25351
|
+
|
|
24227
25352
|
// src/routes/voices.ts
|
|
24228
|
-
import { Hono as
|
|
25353
|
+
import { Hono as Hono14 } from "hono";
|
|
24229
25354
|
function ttsErrorMessage(e, providerLabel) {
|
|
24230
25355
|
if (!(e instanceof Error)) return String(e);
|
|
24231
25356
|
const cause = e.cause;
|
|
@@ -24270,8 +25395,15 @@ function ttsCacheSet(key, val) {
|
|
|
24270
25395
|
}
|
|
24271
25396
|
}
|
|
24272
25397
|
function voicesRouter() {
|
|
24273
|
-
const r = new
|
|
24274
|
-
r.get("/", async (c) =>
|
|
25398
|
+
const r = new Hono14();
|
|
25399
|
+
r.get("/", async (c) => {
|
|
25400
|
+
const catalog = await listAvailableVoices();
|
|
25401
|
+
return c.json({
|
|
25402
|
+
voices: catalog.voices,
|
|
25403
|
+
provider: catalog.provider,
|
|
25404
|
+
configured: catalog.configured
|
|
25405
|
+
});
|
|
25406
|
+
});
|
|
24275
25407
|
r.get("/message/:id/audio", (c) => {
|
|
24276
25408
|
const messageId = c.req.param("id");
|
|
24277
25409
|
const row = getUsableMessageVoice(messageId);
|
|
@@ -24411,7 +25543,7 @@ function voicesRouter() {
|
|
|
24411
25543
|
init_paths();
|
|
24412
25544
|
|
|
24413
25545
|
// src/version.ts
|
|
24414
|
-
var VERSION = "0.1.
|
|
25546
|
+
var VERSION = "0.1.32";
|
|
24415
25547
|
|
|
24416
25548
|
// src/utils/render-picker-catalog.ts
|
|
24417
25549
|
function renderPickerCatalog() {
|
|
@@ -24423,7 +25555,7 @@ function renderPickerCatalog() {
|
|
|
24423
25555
|
|
|
24424
25556
|
// src/server.ts
|
|
24425
25557
|
function createApp() {
|
|
24426
|
-
const app = new
|
|
25558
|
+
const app = new Hono15();
|
|
24427
25559
|
const dir = publicDir();
|
|
24428
25560
|
if (!existsSync2(dir)) {
|
|
24429
25561
|
throw new Error(
|
|
@@ -24471,7 +25603,9 @@ Build the package or check that public/ is bundled alongside dist/.`
|
|
|
24471
25603
|
app.route("/api/avatar", avatarRouter());
|
|
24472
25604
|
app.route("/api/usage", usageRouter());
|
|
24473
25605
|
app.route("/api/voices", voicesRouter());
|
|
25606
|
+
app.route("/api/voice-credentials", voiceCredentialsRouter());
|
|
24474
25607
|
app.route("/api/search", searchRouter());
|
|
25608
|
+
app.route("/api/search-credentials", searchCredentialsRouter());
|
|
24475
25609
|
app.use(
|
|
24476
25610
|
"/*",
|
|
24477
25611
|
serveStatic({
|
|
@@ -24492,6 +25626,10 @@ async function startServer(opts) {
|
|
|
24492
25626
|
url: `http://${host}:${opts.port}`,
|
|
24493
25627
|
close: () => new Promise((resolve2, reject) => {
|
|
24494
25628
|
server.close((err2) => err2 ? reject(err2) : resolve2());
|
|
25629
|
+
const maybeForceClose = server.closeAllConnections;
|
|
25630
|
+
if (typeof maybeForceClose === "function") {
|
|
25631
|
+
maybeForceClose.call(server);
|
|
25632
|
+
}
|
|
24495
25633
|
})
|
|
24496
25634
|
};
|
|
24497
25635
|
}
|