create-workframe 0.1.18 → 0.1.20
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/README.md +1 -1
- package/bin/create-workframe.js +73 -9
- package/package.json +1 -1
- package/workframe-api/action_proxy.py +8 -0
- package/workframe-api/chat_sessions.py +37 -6
- package/workframe-api/chat_stream.py +16 -0
- package/workframe-api/db_schema.py +29 -0
- package/workframe-api/handler_modules/handler_admin.py +32 -0
- package/workframe-api/handler_modules/handler_auth.py +6 -4
- package/workframe-api/handler_modules/handler_chat.py +17 -26
- package/workframe-api/handler_modules/handler_provider.py +13 -14
- package/workframe-api/handler_modules/handler_workspace.py +60 -2
- package/workframe-api/hermes_profiles.py +31 -9
- package/workframe-api/llm_proxy.py +18 -0
- package/workframe-api/mention_invoke.py +29 -24
- package/workframe-api/message_reactions.py +139 -0
- package/workframe-api/model_surface.py +329 -113
- package/workframe-api/package.json +4 -4
- package/workframe-api/profile_gateway.py +14 -6
- package/workframe-api/provider_bindings.py +118 -17
- package/workframe-api/provider_bootstrap.py +74 -38
- package/workframe-api/provider_model_catalog.py +261 -0
- package/workframe-api/rooms.py +4 -3
- package/workframe-api/route_registry.py +4 -0
- package/workframe-api/runtime_cohort.py +118 -23
- package/workframe-api/scripts/run-python.mjs +28 -0
- package/workframe-api/server.py +44 -12
- package/workframe-api/test_agent_identity_authority.py +96 -0
- package/workframe-api/test_chat_attribution_history.py +59 -0
- package/workframe-api/test_enrich_room_messages.py +43 -2
- package/workframe-api/test_llm_proxy_encoding.py +33 -0
- package/workframe-api/test_message_reactions.py +151 -0
- package/workframe-api/test_model_routing_reconcile.py +122 -0
- package/workframe-api/test_model_surface_consistency.py +45 -5
- package/workframe-api/test_oauth_llm.py +1 -1
- package/workframe-api/test_profile_gateway_health.py +37 -0
- package/workframe-api/test_provider_bindings.py +115 -1
- package/workframe-api/test_provider_model_catalog.py +27 -0
- package/workframe-api/test_provider_model_surface.py +2 -2
- package/workframe-api/test_runtime_lease_lifecycle.py +93 -0
- package/workframe-api/test_server_reexports.py +2 -0
- package/workframe-api/test_user_llm_prefs.py +27 -0
- package/workframe-api/test_workspace_file_actions.py +119 -0
- package/workframe-api/turn_credentials.py +41 -0
- package/workframe-api/turn_overlay.py +95 -34
- package/workframe-api/user_prefs.py +15 -0
- package/workframe-api/workspace_bootstrap.py +1 -0
- package/workframe-api/workspace_files.py +124 -0
- package/workframe-ui/public/assets/{arc-CSddOtMa.js → arc-BkATcoqt.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-yusDFKmK.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-BSiUei3D.js → architectureDiagram-3BPJPVTR-31_yAiUS.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BhKTULPo.js → blockDiagram-GPEHLZMM-ClM6Si9T.js} +1 -1
- package/workframe-ui/public/assets/{brave-B885JYnH.svg → brave-BoYMbZi1.svg} +0 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-Bv_xZi-a.js → c4Diagram-AAUBKEIU-DJzEktF3.js} +1 -1
- package/workframe-ui/public/assets/channel-Nb_UjZ3O.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Bh0WiqFE.js → chunk-2J33WTMH-DPFiyb5O.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-qN09pRuK.js → chunk-3OPIFGDE-DXNJzuCn.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-Dz6FVuMa.js → chunk-4BX2VUAB-DDNibGdr.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-DWtPHVfR.js → chunk-55IACEB6-Co3_ulwA.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-CyOUeDAo.js → chunk-5ZQYHXKU-Cmxz8o4d.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BrICvWwx.js → chunk-727SXJPM-CDAQGesU.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CQu2SmLM.js → chunk-AQP2D5EJ-x-swy3kc.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-uJbYXNNG.js → chunk-BSJP7CBP-28nU0JOE.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-CCS_FtVZ.js → chunk-CSCIHK7Q-FUURFika.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-CoLUtPiK.js → chunk-FMBD7UC4-B4RgRO2R.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-DLW4gvEW.js → chunk-KSCS5N6A-CkuppW3T.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-CMt0LJAd.js → chunk-L5ZTLDWV-DyrPSaz3.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-Bn-maIlt.js → chunk-LZXEDZCA-DFaZSDNV.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-CG7yoApR.js → chunk-ND2GUHAM-DXI0OJxg.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-B4dIvbii.js → chunk-NZK2D7GU-CyOp1ZG2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-B5spDyXy.js → chunk-O5CBEL6O-tZZMCfbx.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C_TtOXi5.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-BjSJQEIY.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-D5c0TMX6.js → chunk-XPW4576I-B7_2gS3x.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-JD6iZUGh.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-JD6iZUGh.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-DFRM3DZv.js → cose-bilkent-S5V4N54A-BWJaHw_L.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-CnUdKeJa.js → dagre-BM42HDAG-Bu5MadGV.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ds3-e-hW.js → diagram-2AECGRRQ-ckLZkDFD.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-B41zx8FF.js → diagram-5GNKFQAL-Cf6Abx1Y.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-D83tI9I8.js → diagram-KO2AKTUF-TLk7_5mJ.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-h_EuFyLI.js → diagram-LMA3HP47-ouNxsAb0.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-CpU-d3RZ.js → diagram-OG6HWLK6-BMCBQ35D.js} +1 -1
- package/workframe-ui/public/assets/{dist-_igeSJ9r.js → dist-C9bxMexz.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-jLFoncRU.js → erDiagram-TEJ5UH35-BN3e6jb1.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Bqd_sTz0.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-C9IaNgCy.js → flowDiagram-I6XJVG4X-BiinzZrd.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-A9FhfFc5.js → ganttDiagram-6RSMTGT7-AYE8oBBG.js} +1 -1
- package/workframe-ui/public/assets/{gemini-Cq-0-rmr.svg → gemini-DqnZlvMo.svg} +0 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BqK_IjZb.js → gitGraph-WXDBUCRP-mIq_E2vs.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CDVqxhyW.js → gitGraphDiagram-PVQCEYII-Cf8IFEcO.js} +1 -1
- package/workframe-ui/public/assets/index-BnfFRGR3.js +133 -0
- package/workframe-ui/public/assets/index-COshPjF3.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-C2EKRglL.js → info-J43DQDTF--_3kCaIQ.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-azQtFzpU.js → infoDiagram-5YYISTIA-BuIOyYMu.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-BuDOLv1t.js → ishikawaDiagram-YF4QCWOH-D3bbsXQ8.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-ypTHFoT0.js → journeyDiagram-JHISSGLW-BrZ26C8L.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Cgr78J_x.js → kanban-definition-UN3LZRKU-CJiUyUOr.js} +1 -1
- package/workframe-ui/public/assets/{line-CcwgaMdv.js → line-Bee1snmU.js} +1 -1
- package/workframe-ui/public/assets/{linear-CswIlWaW.js → linear-dfD-zlI7.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-CQYhP37X.js → mermaid-parser.core-D6IqqDSH.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-Cz1XNBaK.js → mermaid.core-CuuxYt2r.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-BJLu0829.js → mindmap-definition-RKZ34NQL-T7W2fkwA.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-AB1qh96V.js → packet-YPE3B663-CiXMDasj.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-CokIXLdT.js → pie-LRSECV5Y-CUx1_96f.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyrwQn5F.js → pieDiagram-4H26LBE5-DfR96FUO.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-C_Fdg3F6.js → quadrantDiagram-W4KKPZXB-Dj6mqgQE.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-C21Xfrrg.js → radar-GUYGQ44K-DKT2q98y.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-ChhaWgaF.js → requirementDiagram-4Y6WPE33-DYDnBVw9.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-C5T4sDTt.js → sankeyDiagram-5OEKKPKP-BSkxpjjO.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DRks8i3A.js → sequenceDiagram-3UESZ5HK-Dz6SNH9S.js} +1 -1
- package/workframe-ui/public/assets/{src-BQu5waHd.js → src-DC521nE_.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BI-AKYta.js → stateDiagram-AJRCARHV-D4ZoF5NP.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DknveEiR.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DnAIWzEc.js → timeline-definition-PNZ67QCA-d0ezg1Hu.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-BEF_sjx7.js → treeView-BLDUP644-kXyaqwIy.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-BcbL7g5c.js → treemap-LRROVOQU-g168p7-O.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-DAoIbCkz.js → vennDiagram-CIIHVFJN-C0_QsmRl.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-3SOmFBFw.js → wardley-L42UT6IY-BNFCdzxQ.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DlQV_-LH.js → wardleyDiagram-YWT4CUSO-BOwUS9sY.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-CVoTTRHF.js → xychartDiagram-2RQKCTM6-Cxr1ZiKh.js} +1 -1
- package/workframe-ui/public/index.html +2 -2
- package/workframe-ui/public/workframe-build.json +3 -3
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CDDNM1ni.js +0 -1
- package/workframe-ui/public/assets/channel-R52LrLO3.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C_9MbT10.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-VJadzJmf.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CpBjaI3g.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CpBjaI3g.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-kzOxPvtj.js +0 -1
- package/workframe-ui/public/assets/index-Bco9IDMA.css +0 -1
- package/workframe-ui/public/assets/index-Do34PZS2.js +0 -132
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-vJyhFZbx.js +0 -1
package/README.md
CHANGED
package/bin/create-workframe.js
CHANGED
|
@@ -1360,10 +1360,27 @@ echo "Done. Mission control and workframe unchanged unless you recreate the full
|
|
|
1360
1360
|
`;
|
|
1361
1361
|
}
|
|
1362
1362
|
|
|
1363
|
-
function launchPhaseBInstaller(target) {
|
|
1363
|
+
function launchPhaseBInstaller(target, { wait = false, noBrowser = false } = {}) {
|
|
1364
1364
|
if (process.platform === 'win32') {
|
|
1365
1365
|
const script = path.join(target, 'scripts', 'start-install.ps1');
|
|
1366
1366
|
if (!fs.existsSync(script)) return false;
|
|
1367
|
+
if (wait) {
|
|
1368
|
+
const args = [
|
|
1369
|
+
'-NoProfile',
|
|
1370
|
+
'-ExecutionPolicy',
|
|
1371
|
+
'Bypass',
|
|
1372
|
+
'-File',
|
|
1373
|
+
script,
|
|
1374
|
+
'-NoPrompt',
|
|
1375
|
+
];
|
|
1376
|
+
if (noBrowser) args.push('-NoBrowser');
|
|
1377
|
+
const child = spawnSync('powershell.exe', args, { cwd: target, stdio: 'inherit' });
|
|
1378
|
+
if (child.error) throw child.error;
|
|
1379
|
+
if (child.status !== 0) {
|
|
1380
|
+
throw new Error(`Phase B installer failed (${child.status ?? 'unknown exit'})`);
|
|
1381
|
+
}
|
|
1382
|
+
return true;
|
|
1383
|
+
}
|
|
1367
1384
|
const escaped = script.replace(/'/g, "''");
|
|
1368
1385
|
const child = spawn(
|
|
1369
1386
|
'powershell.exe',
|
|
@@ -1386,6 +1403,16 @@ function launchPhaseBInstaller(target) {
|
|
|
1386
1403
|
}
|
|
1387
1404
|
const script = path.join(target, 'scripts', 'start-install.sh');
|
|
1388
1405
|
if (!fs.existsSync(script)) return false;
|
|
1406
|
+
if (wait) {
|
|
1407
|
+
const args = ['--no-prompt'];
|
|
1408
|
+
if (noBrowser) args.push('--no-browser');
|
|
1409
|
+
const child = spawnSync(script, args, { cwd: target, stdio: 'inherit' });
|
|
1410
|
+
if (child.error) throw child.error;
|
|
1411
|
+
if (child.status !== 0) {
|
|
1412
|
+
throw new Error(`Phase B installer failed (${child.status ?? 'unknown exit'})`);
|
|
1413
|
+
}
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1389
1416
|
const child = spawn(script, [], {
|
|
1390
1417
|
cwd: target,
|
|
1391
1418
|
detached: true,
|
|
@@ -1520,7 +1547,12 @@ ${openWorkframeBrowserBlockSh()}
|
|
|
1520
1547
|
}
|
|
1521
1548
|
|
|
1522
1549
|
function startInstallPs1(docker, nativeProfile, nativeAgentName, ports) {
|
|
1523
|
-
return
|
|
1550
|
+
return `param(
|
|
1551
|
+
[switch]$NoBrowser,
|
|
1552
|
+
[switch]$NoPrompt
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
$ErrorActionPreference = 'Stop'
|
|
1524
1556
|
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
|
1525
1557
|
Set-Location $Root
|
|
1526
1558
|
|
|
@@ -1533,7 +1565,9 @@ Write-Host " Project: ${nativeAgentName}" -ForegroundColor Cyan
|
|
|
1533
1565
|
Write-Host '========================================' -ForegroundColor Cyan
|
|
1534
1566
|
Write-Host ''
|
|
1535
1567
|
|
|
1536
|
-
|
|
1568
|
+
if (-not $NoBrowser) {
|
|
1569
|
+
& "$Root\\scripts\\open-install-ui.ps1"
|
|
1570
|
+
}
|
|
1537
1571
|
|
|
1538
1572
|
Write-Host 'Step 1/2 - Starting Docker stack...' -ForegroundColor Yellow
|
|
1539
1573
|
${composeUpPs1Block()}
|
|
@@ -1558,8 +1592,14 @@ Write-Host ' Workframe UI: http://127.0.0.1:${ports.ui}'
|
|
|
1558
1592
|
Write-Host ' Hermes dashboard: http://127.0.0.1:${ports.dashboard}'
|
|
1559
1593
|
Write-Host ''
|
|
1560
1594
|
Write-Host 'LLM keys are configured in the onboarding UI - no Hermes TUI required.' -ForegroundColor DarkGray
|
|
1561
|
-
${
|
|
1562
|
-
|
|
1595
|
+
if (-not $NoBrowser) {
|
|
1596
|
+
Write-Host ''
|
|
1597
|
+
Write-Host 'Opening Workframe setup wizard (/install)...' -ForegroundColor Green
|
|
1598
|
+
& "$Root\\scripts\\open-install-ui.ps1"
|
|
1599
|
+
}
|
|
1600
|
+
if (-not $NoPrompt) {
|
|
1601
|
+
Read-Host 'Press Enter to close'
|
|
1602
|
+
}
|
|
1563
1603
|
`;
|
|
1564
1604
|
}
|
|
1565
1605
|
|
|
@@ -1569,6 +1609,15 @@ set -euo pipefail
|
|
|
1569
1609
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
1570
1610
|
cd "$ROOT"
|
|
1571
1611
|
|
|
1612
|
+
NO_BROWSER=0
|
|
1613
|
+
for arg in "$@"; do
|
|
1614
|
+
case "$arg" in
|
|
1615
|
+
--no-browser) NO_BROWSER=1 ;;
|
|
1616
|
+
--no-prompt) ;;
|
|
1617
|
+
*) echo "Unknown option: $arg" >&2; exit 2 ;;
|
|
1618
|
+
esac
|
|
1619
|
+
done
|
|
1620
|
+
|
|
1572
1621
|
mkdir -p Agents Files
|
|
1573
1622
|
|
|
1574
1623
|
echo ""
|
|
@@ -1578,7 +1627,9 @@ echo " Project: ${nativeAgentName}"
|
|
|
1578
1627
|
echo "========================================"
|
|
1579
1628
|
echo ""
|
|
1580
1629
|
|
|
1581
|
-
"$
|
|
1630
|
+
if [ "$NO_BROWSER" -eq 0 ]; then
|
|
1631
|
+
"$ROOT/scripts/open-install-ui.sh" || true
|
|
1632
|
+
fi
|
|
1582
1633
|
|
|
1583
1634
|
echo "Step 1/2 — Starting Docker stack..."
|
|
1584
1635
|
${composeUpBashBlock()}
|
|
@@ -1598,7 +1649,11 @@ echo " Workframe UI: http://127.0.0.1:${ports.ui}"
|
|
|
1598
1649
|
echo " Hermes dashboard: http://127.0.0.1:${ports.dashboard}/"
|
|
1599
1650
|
echo ""
|
|
1600
1651
|
echo "LLM keys are configured in the onboarding UI - no Hermes TUI required."
|
|
1601
|
-
$
|
|
1652
|
+
if [ "$NO_BROWSER" -eq 0 ]; then
|
|
1653
|
+
echo ""
|
|
1654
|
+
echo "Opening Workframe setup wizard (/install)..."
|
|
1655
|
+
"$ROOT/scripts/open-install-ui.sh" || true
|
|
1656
|
+
fi
|
|
1602
1657
|
`;
|
|
1603
1658
|
}
|
|
1604
1659
|
|
|
@@ -1827,6 +1882,8 @@ function parseArgs(argv) {
|
|
|
1827
1882
|
else if (a === '--run-docker-pull') args.runDockerPull = true;
|
|
1828
1883
|
else if (a === '--allow-install-actions') args.installGuideOnly = false;
|
|
1829
1884
|
else if (a === '--no-launch') args.noLaunch = true;
|
|
1885
|
+
else if (a === '--wait') args.waitForInstall = true;
|
|
1886
|
+
else if (a === '--no-browser') args.noBrowser = true;
|
|
1830
1887
|
else if (a === '--slot') args.slot = Number(argv[++i]);
|
|
1831
1888
|
else if (a.startsWith('--slot=')) args.slot = Number(a.split('=', 2)[1]);
|
|
1832
1889
|
else if (a === '--deploy') args.deploy = argv[++i];
|
|
@@ -1867,6 +1924,8 @@ Flags:
|
|
|
1867
1924
|
--allow-install-actions Allow installer to execute shell install actions
|
|
1868
1925
|
-y, --yes Non-interactive defaults
|
|
1869
1926
|
--no-launch Scaffold only; do not open Phase B installer
|
|
1927
|
+
--wait Run Phase B here and fail if installation fails
|
|
1928
|
+
--no-browser With --wait, do not open the system browser
|
|
1870
1929
|
`);
|
|
1871
1930
|
}
|
|
1872
1931
|
|
|
@@ -2799,9 +2858,14 @@ async function main() {
|
|
|
2799
2858
|
console.log(`Bootstrap default: bootstrap-native (full pack: ${profiles.length} profiles)`);
|
|
2800
2859
|
console.log(`SOUL seeds: ${seedProfiles.length} profiles + native SETUP playbook`);
|
|
2801
2860
|
if (!args.ci && !args.noLaunch) {
|
|
2802
|
-
if (launchPhaseBInstaller(target
|
|
2861
|
+
if (launchPhaseBInstaller(target, {
|
|
2862
|
+
wait: Boolean(args.waitForInstall),
|
|
2863
|
+
noBrowser: Boolean(args.noBrowser),
|
|
2864
|
+
})) {
|
|
2803
2865
|
console.log('');
|
|
2804
|
-
console.log(
|
|
2866
|
+
console.log(args.waitForInstall
|
|
2867
|
+
? 'Installer completed successfully.'
|
|
2868
|
+
: 'Launching installer — browser opens /install immediately.');
|
|
2805
2869
|
console.log(`Continue setup at http://127.0.0.1:${ports.ui}/install`);
|
|
2806
2870
|
}
|
|
2807
2871
|
} else if (args.noLaunch) {
|
package/package.json
CHANGED
|
@@ -82,8 +82,10 @@ def forward_request(
|
|
|
82
82
|
"content-length",
|
|
83
83
|
"authorization",
|
|
84
84
|
"x-api-key",
|
|
85
|
+
"accept-encoding",
|
|
85
86
|
}
|
|
86
87
|
}
|
|
88
|
+
upstream_headers["Accept-Encoding"] = "identity"
|
|
87
89
|
upstream_headers.update(upstream_auth_header(provider, secret))
|
|
88
90
|
|
|
89
91
|
req = urllib.request.Request(url, data=body, headers=upstream_headers, method=method.upper())
|
|
@@ -91,10 +93,16 @@ def forward_request(
|
|
|
91
93
|
with urllib.request.urlopen(req, timeout=600) as resp:
|
|
92
94
|
resp_body = resp.read()
|
|
93
95
|
out_headers = {"Content-Type": resp.headers.get("Content-Type", "application/octet-stream")}
|
|
96
|
+
content_encoding = resp.headers.get("Content-Encoding", "").strip()
|
|
97
|
+
if content_encoding:
|
|
98
|
+
out_headers["Content-Encoding"] = content_encoding
|
|
94
99
|
return resp.status, out_headers, resp_body
|
|
95
100
|
except urllib.error.HTTPError as exc:
|
|
96
101
|
raw = exc.read()
|
|
97
102
|
out_headers = {"Content-Type": exc.headers.get("Content-Type", "application/json")}
|
|
103
|
+
content_encoding = exc.headers.get("Content-Encoding", "").strip()
|
|
104
|
+
if content_encoding:
|
|
105
|
+
out_headers["Content-Encoding"] = content_encoding
|
|
98
106
|
return exc.code, out_headers, raw
|
|
99
107
|
|
|
100
108
|
|
|
@@ -213,6 +213,34 @@ def _llm_attribution_for_profile(profile: str, session_model: str = "") -> tuple
|
|
|
213
213
|
return model, str(billing or "").strip()
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def _session_run_attributions(profile: str, session_id: str) -> list[tuple[str, str]]:
|
|
217
|
+
"""Completed-turn model/provider pairs in the same order as chat turns."""
|
|
218
|
+
prof = str(profile or "").strip()
|
|
219
|
+
sid = str(session_id or "").strip()
|
|
220
|
+
if not prof or not sid:
|
|
221
|
+
return []
|
|
222
|
+
conn = _srv()._workframe_db()
|
|
223
|
+
try:
|
|
224
|
+
rows = conn.execute(
|
|
225
|
+
"""
|
|
226
|
+
SELECT li.model, li.provider
|
|
227
|
+
FROM runs r
|
|
228
|
+
JOIN run_line_items li ON li.run_id = r.run_id AND li.kind = 'llm_turn'
|
|
229
|
+
WHERE r.session_id = ? AND r.profile_slug = ? AND r.status = 'completed'
|
|
230
|
+
ORDER BY r.created_at ASC, li.created_at ASC, li.line_item_id ASC
|
|
231
|
+
""",
|
|
232
|
+
(sid, prof),
|
|
233
|
+
).fetchall()
|
|
234
|
+
except sqlite3.Error:
|
|
235
|
+
return []
|
|
236
|
+
finally:
|
|
237
|
+
conn.close()
|
|
238
|
+
return [
|
|
239
|
+
(str(row["model"] or "").strip(), str(row["provider"] or "").strip())
|
|
240
|
+
for row in rows
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
|
|
216
244
|
def _latest_session_id(profile: str) -> str:
|
|
217
245
|
"""Most recent session — prefer active (ended_at IS NULL), then by started_at."""
|
|
218
246
|
db = _srv()._profile_dir(profile) / "state.db"
|
|
@@ -441,7 +469,8 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
|
|
|
441
469
|
_srv()._runtime_template_slug(profile) if _srv()._is_runtime_profile_slug(profile) else profile
|
|
442
470
|
)
|
|
443
471
|
display = _srv()._profile_display_name(profile)
|
|
444
|
-
|
|
472
|
+
run_attributions = _session_run_attributions(profile, sid)
|
|
473
|
+
agent_turn_index = 0
|
|
445
474
|
try:
|
|
446
475
|
raw = conn.execute(
|
|
447
476
|
"""
|
|
@@ -483,12 +512,14 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
|
|
|
483
512
|
if current and current["role"] == "agent":
|
|
484
513
|
current["segments"].extend(segments)
|
|
485
514
|
current["tokens"] += tokens
|
|
486
|
-
if attr_model:
|
|
487
|
-
current["model"] = attr_model
|
|
488
|
-
current["llm_provider"] = attr_provider
|
|
489
515
|
else:
|
|
490
516
|
if current:
|
|
491
517
|
turns.append(current)
|
|
518
|
+
if agent_turn_index < len(run_attributions):
|
|
519
|
+
attr_model, attr_provider = run_attributions[agent_turn_index]
|
|
520
|
+
else:
|
|
521
|
+
attr_model, attr_provider = "", ""
|
|
522
|
+
agent_turn_index += 1
|
|
492
523
|
current = {
|
|
493
524
|
"id": f"turn-{m['id']}",
|
|
494
525
|
"authorId": author_profile,
|
|
@@ -497,8 +528,8 @@ def chat_messages(profile: str, session_id: str = "", source_id: str = "ui") ->
|
|
|
497
528
|
"segments": list(segments),
|
|
498
529
|
"timestamp": ts,
|
|
499
530
|
"tokens": tokens,
|
|
500
|
-
"model": attr_model,
|
|
501
|
-
"llm_provider": attr_provider,
|
|
531
|
+
**({"model": attr_model} if attr_model else {}),
|
|
532
|
+
**({"llm_provider": attr_provider} if attr_provider else {}),
|
|
502
533
|
}
|
|
503
534
|
if current:
|
|
504
535
|
turns.append(current)
|
|
@@ -391,6 +391,22 @@ def stream_profile_chat(handler: BaseHTTPRequestHandler, profile: str, payload:
|
|
|
391
391
|
raise ValueError("session_id required")
|
|
392
392
|
if not text:
|
|
393
393
|
raise ValueError("text required")
|
|
394
|
+
# Bootstrap before reading the model block. Existing generated profiles may
|
|
395
|
+
# need provider/model migrations; reading first dispatches one stale turn.
|
|
396
|
+
bootstrapped = _srv()._bootstrap_profile_providers(prof, _triggering_user, _workspace_id)
|
|
397
|
+
# Native/template profiles can still contain the installer default
|
|
398
|
+
# (provider=openrouter + public base_url). A user vault credential must
|
|
399
|
+
# always be consumed through Workframe's authenticated proxy.
|
|
400
|
+
if _triggering_user:
|
|
401
|
+
reconciled = _srv()._reconcile_profile_llm_for_user(
|
|
402
|
+
prof, _triggering_user, _workspace_id,
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
reconciled = False
|
|
406
|
+
if bootstrapped and not reconciled:
|
|
407
|
+
# Hermes caches config.yaml. Reload before dispatching so the first
|
|
408
|
+
# turn after a repair or model save cannot use stale routing.
|
|
409
|
+
_srv()._reload_runtime_profile_gateway(prof, wait_healthy=True)
|
|
394
410
|
model_block = _srv()._read_model_block(prof)
|
|
395
411
|
llm_provider = _srv()._llm_billing_provider(
|
|
396
412
|
prof, user_id=_triggering_user, workspace_id=_workspace_id, block=model_block,
|
|
@@ -588,6 +588,33 @@ def _migrate_v13_workspace_members_in_spaces(conn: sqlite3.Connection) -> None:
|
|
|
588
588
|
VALUES ('13', 'workspace members joined to all space rooms', datetime('now'))
|
|
589
589
|
""",
|
|
590
590
|
)
|
|
591
|
+
def _migrate_v15_message_reactions(conn: sqlite3.Connection) -> None:
|
|
592
|
+
if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '15' LIMIT 1").fetchone():
|
|
593
|
+
return
|
|
594
|
+
conn.execute(
|
|
595
|
+
"""
|
|
596
|
+
CREATE TABLE IF NOT EXISTS message_reactions (
|
|
597
|
+
scope_key TEXT NOT NULL,
|
|
598
|
+
message_id TEXT NOT NULL,
|
|
599
|
+
user_id TEXT NOT NULL,
|
|
600
|
+
emoji TEXT NOT NULL,
|
|
601
|
+
created_at TEXT NOT NULL,
|
|
602
|
+
PRIMARY KEY (scope_key, message_id, user_id, emoji)
|
|
603
|
+
)
|
|
604
|
+
"""
|
|
605
|
+
)
|
|
606
|
+
conn.execute(
|
|
607
|
+
"""
|
|
608
|
+
CREATE INDEX IF NOT EXISTS idx_message_reactions_scope_message
|
|
609
|
+
ON message_reactions(scope_key, message_id, created_at)
|
|
610
|
+
"""
|
|
611
|
+
)
|
|
612
|
+
conn.execute(
|
|
613
|
+
"""
|
|
614
|
+
INSERT INTO schema_migrations (version, description, applied_at)
|
|
615
|
+
VALUES ('15', 'message reactions for room and agent-DM chat surfaces', datetime('now'))
|
|
616
|
+
""",
|
|
617
|
+
)
|
|
591
618
|
def _migrate_v8_room_avatars(conn: sqlite3.Connection) -> None:
|
|
592
619
|
if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '8' LIMIT 1").fetchone():
|
|
593
620
|
return
|
|
@@ -738,6 +765,7 @@ def ensure_workframe_db_schema(open_db: OpenDb) -> None:
|
|
|
738
765
|
_migrate_v11_delegation_kanban_boards(conn)
|
|
739
766
|
_migrate_v12_adopt_install_keys_to_owners(conn)
|
|
740
767
|
_migrate_v13_workspace_members_in_spaces(conn)
|
|
768
|
+
_migrate_v15_message_reactions(conn)
|
|
741
769
|
conn.commit()
|
|
742
770
|
finally:
|
|
743
771
|
conn.close()
|
|
@@ -750,6 +778,7 @@ def ensure_oauth_pending_migrations(open_db: OpenDb) -> None:
|
|
|
750
778
|
_migrate_v11_delegation_kanban_boards(conn)
|
|
751
779
|
_migrate_v12_adopt_install_keys_to_owners(conn)
|
|
752
780
|
_migrate_v13_workspace_members_in_spaces(conn)
|
|
781
|
+
_migrate_v15_message_reactions(conn)
|
|
753
782
|
conn.commit()
|
|
754
783
|
finally:
|
|
755
784
|
conn.close()
|
|
@@ -197,6 +197,38 @@ class AdminRoutesMixin:
|
|
|
197
197
|
content_b64 = str(body.get("content_base64") or body.get("content") or "")
|
|
198
198
|
self._json(200, srv.file_upload_binary(rel, content_b64))
|
|
199
199
|
|
|
200
|
+
def _route_post_files_archive(self, body: dict) -> None:
|
|
201
|
+
srv = _srv()
|
|
202
|
+
if not srv._check_auth(self):
|
|
203
|
+
self._json(401, {"error": "unauthorized"})
|
|
204
|
+
return
|
|
205
|
+
paths = body.get("paths")
|
|
206
|
+
try:
|
|
207
|
+
archive = srv.files_archive(paths)
|
|
208
|
+
except PermissionError as exc:
|
|
209
|
+
self._json(403, {"ok": False, "error": "protected_file", "detail": str(exc)})
|
|
210
|
+
return
|
|
211
|
+
except ValueError as exc:
|
|
212
|
+
self._json(400, {"ok": False, "error": "invalid_selection", "detail": str(exc)})
|
|
213
|
+
return
|
|
214
|
+
self._send(200, archive, "application/zip")
|
|
215
|
+
|
|
216
|
+
def _route_post_files_delete(self, body: dict) -> None:
|
|
217
|
+
srv = _srv()
|
|
218
|
+
if not srv._check_auth(self):
|
|
219
|
+
self._json(401, {"error": "unauthorized"})
|
|
220
|
+
return
|
|
221
|
+
paths = body.get("paths")
|
|
222
|
+
try:
|
|
223
|
+
result = srv.files_delete(paths)
|
|
224
|
+
except PermissionError as exc:
|
|
225
|
+
self._json(403, {"ok": False, "error": "protected_file", "detail": str(exc)})
|
|
226
|
+
return
|
|
227
|
+
except ValueError as exc:
|
|
228
|
+
self._json(400, {"ok": False, "error": "invalid_selection", "detail": str(exc)})
|
|
229
|
+
return
|
|
230
|
+
self._json(200, result)
|
|
231
|
+
|
|
200
232
|
def _route_get_admin_vault_status(self, qs: dict[str, list[str]]) -> None:
|
|
201
233
|
srv = _srv()
|
|
202
234
|
if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
|
|
@@ -529,13 +529,16 @@ class AuthRoutesMixin:
|
|
|
529
529
|
)
|
|
530
530
|
display = str(body.get("display_name") or "").strip() if "display_name" in body else ""
|
|
531
531
|
tagline = str(body.get("tagline") or "").strip() if "tagline" in body else ""
|
|
532
|
-
srv.
|
|
533
|
-
runtime,
|
|
532
|
+
srv._apply_profile_identity(
|
|
534
533
|
native_slug,
|
|
535
534
|
display_name=display,
|
|
536
535
|
tagline=tagline,
|
|
537
|
-
user_soul=soul,
|
|
538
536
|
)
|
|
537
|
+
if soul:
|
|
538
|
+
result = srv.profile_soul_set(native_slug, soul)
|
|
539
|
+
if not result.get("ok"):
|
|
540
|
+
self._json(400, result)
|
|
541
|
+
return
|
|
539
542
|
profile_patch: dict[str, Any] = {}
|
|
540
543
|
if display:
|
|
541
544
|
profile_patch["display_name"] = display
|
|
@@ -552,6 +555,5 @@ class AuthRoutesMixin:
|
|
|
552
555
|
stamp = datetime.now(timezone.utc).isoformat()
|
|
553
556
|
row_patch = {**avatar_patch, "updated_at": stamp}
|
|
554
557
|
srv._upsert_agent_registry_row(native_slug, row_patch)
|
|
555
|
-
srv._upsert_agent_registry_row(runtime, row_patch)
|
|
556
558
|
srv._sync_agent_profile_db(native_slug, {"avatar_url": avatar_patch.get("avatar_url", "")})
|
|
557
559
|
self._json(200, {"ok": True, "runtime_profile": runtime})
|
|
@@ -223,17 +223,12 @@ class ChatRoutesMixin:
|
|
|
223
223
|
self._json(400, {"error": "model required"})
|
|
224
224
|
return
|
|
225
225
|
billing_provider = str(body.get("billing_provider", "")).strip()
|
|
226
|
-
#
|
|
227
|
-
# the
|
|
228
|
-
#
|
|
229
|
-
if
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
workspaces = srv._get_user_workspaces(user_id)
|
|
233
|
-
current = srv._resolve_current_workspace(user_id, workspaces)
|
|
234
|
-
workspace_id = str((current or {}).get("id") or (workspaces[0] or {}).get("id") or "")
|
|
235
|
-
profile = srv._runtime_profile_slug(user_id, profile)
|
|
236
|
-
srv.ensure_runtime_profile(profile, srv._runtime_template_slug(profile), user_id, workspace_id)
|
|
226
|
+
# Provider/model are shared agent settings. Credentials remain scoped
|
|
227
|
+
# to the acting user or company, so only workspace admins may mutate
|
|
228
|
+
# the agent template; the backend canonicalizes any stale u-* request.
|
|
229
|
+
if not selection_only and not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
|
|
230
|
+
self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
|
|
231
|
+
return
|
|
237
232
|
self._json(
|
|
238
233
|
200,
|
|
239
234
|
hermes_model_set(
|
|
@@ -267,14 +262,7 @@ class ChatRoutesMixin:
|
|
|
267
262
|
chain = body.get("chain", [])
|
|
268
263
|
selection_only = bool(body.get("selection_only"))
|
|
269
264
|
user_id = str(getattr(self, "auth_user", "") or "")
|
|
270
|
-
|
|
271
|
-
if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
|
|
272
|
-
if not workspace_id:
|
|
273
|
-
workspaces = srv._get_user_workspaces(user_id)
|
|
274
|
-
current = srv._resolve_current_workspace(user_id, workspaces)
|
|
275
|
-
workspace_id = str((current or {}).get("id") or (workspaces[0] or {}).get("id") or "")
|
|
276
|
-
profile = srv._runtime_profile_slug(user_id, profile)
|
|
277
|
-
srv.ensure_runtime_profile(profile, srv._runtime_template_slug(profile), user_id, workspace_id)
|
|
265
|
+
workspace_id = str(body.get("workspace_id", "")).strip()
|
|
278
266
|
if not isinstance(chain, list):
|
|
279
267
|
self._json(400, {"error": "chain must be a list"})
|
|
280
268
|
return
|
|
@@ -288,6 +276,7 @@ class ChatRoutesMixin:
|
|
|
288
276
|
chain,
|
|
289
277
|
selection_only=selection_only,
|
|
290
278
|
user_id=user_id,
|
|
279
|
+
workspace_id=workspace_id,
|
|
291
280
|
),
|
|
292
281
|
)
|
|
293
282
|
|
|
@@ -546,9 +535,7 @@ class ChatRoutesMixin:
|
|
|
546
535
|
return
|
|
547
536
|
template = resolve_validated_profile(profile_bootstrap_post.group(1))
|
|
548
537
|
user_id = str(getattr(self, "auth_user", "") or "").strip()
|
|
549
|
-
|
|
550
|
-
self._json(403, {"ok": False, "error": "native_agent_identity_owner_only"})
|
|
551
|
-
return
|
|
538
|
+
can_manage_identity = srv._role_allows(self, srv.OWNER_ADMIN_ROLES)
|
|
552
539
|
workspace_id = str(body.get("workspace_id") or "").strip()
|
|
553
540
|
if not workspace_id and user_id:
|
|
554
541
|
workspaces = srv._get_user_workspaces(user_id)
|
|
@@ -558,10 +545,14 @@ class ChatRoutesMixin:
|
|
|
558
545
|
self._json(400, {"error": "workspace_id and session required"})
|
|
559
546
|
return
|
|
560
547
|
model = str(body.get("model") or "").strip()
|
|
561
|
-
soul = str(body.get("soul") or "").strip()
|
|
562
|
-
display_name =
|
|
563
|
-
|
|
564
|
-
|
|
548
|
+
soul = str(body.get("soul") or "").strip() if can_manage_identity else ""
|
|
549
|
+
display_name = (
|
|
550
|
+
str(body.get("display_name") or body.get("room_name") or "").strip()
|
|
551
|
+
if can_manage_identity
|
|
552
|
+
else ""
|
|
553
|
+
)
|
|
554
|
+
role = str(body.get("role") or "").strip() if can_manage_identity else ""
|
|
555
|
+
tagline = str(body.get("tagline") or "").strip() if can_manage_identity else ""
|
|
565
556
|
lane = bootstrap_agent_dm_lane(
|
|
566
557
|
user_id,
|
|
567
558
|
workspace_id,
|
|
@@ -8,7 +8,6 @@ from typing import Any
|
|
|
8
8
|
import action_proxy
|
|
9
9
|
import internal_proxy_auth
|
|
10
10
|
import llm_proxy
|
|
11
|
-
import openrouter_catalog
|
|
12
11
|
import platform_auth
|
|
13
12
|
import run_surface_wiring
|
|
14
13
|
|
|
@@ -125,7 +124,8 @@ class ProviderRoutesMixin:
|
|
|
125
124
|
self._json(401, {"ok": False, "error": "no_session"})
|
|
126
125
|
return
|
|
127
126
|
workspace_id = qs.get("workspace_id", [""])[0]
|
|
128
|
-
|
|
127
|
+
credential_scope = qs.get("credential_scope", ["effective"])[0]
|
|
128
|
+
self._json(200, srv.list_user_providers(user_id, workspace_id, credential_scope))
|
|
129
129
|
|
|
130
130
|
def _route_post_me_credentials(self, body: dict) -> None:
|
|
131
131
|
srv = _srv()
|
|
@@ -191,19 +191,18 @@ class ProviderRoutesMixin:
|
|
|
191
191
|
self._json(500, {"ok": False, "error": f"db_error: {exc}"})
|
|
192
192
|
return
|
|
193
193
|
srv._invalidate_user_llm_picker_cache(user_id)
|
|
194
|
+
srv._revoke_runtime_llm_leases(payer_user_id=user_id, provider=provider)
|
|
194
195
|
self._log_audit("credential_stored", "credential_binding", cred_id, f"provider={provider}")
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if secret_probe:
|
|
206
|
-
health = openrouter_catalog.probe_account(secret_probe)
|
|
196
|
+
# Credential persistence must not depend on an upstream network round-trip.
|
|
197
|
+
# OpenRouter's account endpoints can each consume their full timeout when a
|
|
198
|
+
# self-hosted install has restricted egress, leaving the settings UI stuck
|
|
199
|
+
# in "Saving…" even though the encrypted credential is already committed.
|
|
200
|
+
# Model/catalog requests provide the first real provider health signal.
|
|
201
|
+
health: dict[str, Any] = {
|
|
202
|
+
"ok": True,
|
|
203
|
+
"provider": provider,
|
|
204
|
+
"status": "stored",
|
|
205
|
+
}
|
|
207
206
|
try:
|
|
208
207
|
srv._bootstrap_model_after_llm_connect(user_id, str(body.get("workspace_id") or ""), provider)
|
|
209
208
|
except (OSError, RuntimeError, ValueError) as exc:
|
|
@@ -15,6 +15,7 @@ import uuid
|
|
|
15
15
|
import activity_feed
|
|
16
16
|
import api_errors
|
|
17
17
|
import chat_bind
|
|
18
|
+
import message_reactions
|
|
18
19
|
from email_sender import APP_BASE_URL, send_branded_invite_email
|
|
19
20
|
|
|
20
21
|
list_room_sessions = chat_bind.list_room_sessions
|
|
@@ -31,6 +32,56 @@ def _srv():
|
|
|
31
32
|
|
|
32
33
|
class WorkspaceRoutesMixin:
|
|
33
34
|
|
|
35
|
+
def _route_get_message_reactions(self, qs: dict[str, list[str]]) -> None:
|
|
36
|
+
srv = _srv()
|
|
37
|
+
user_id = str(getattr(self, "auth_user", "") or "")
|
|
38
|
+
if not user_id:
|
|
39
|
+
self._json(401, {"ok": False, "error": "no_session"})
|
|
40
|
+
return
|
|
41
|
+
conn = srv._workframe_db()
|
|
42
|
+
try:
|
|
43
|
+
payload = message_reactions.list_reactions(
|
|
44
|
+
conn,
|
|
45
|
+
qs.get("scope", [""])[0],
|
|
46
|
+
user_id,
|
|
47
|
+
srv._user_can_access_room,
|
|
48
|
+
)
|
|
49
|
+
except PermissionError:
|
|
50
|
+
self._json(403, {"ok": False, "error": "forbidden"})
|
|
51
|
+
return
|
|
52
|
+
except ValueError as exc:
|
|
53
|
+
self._json(400, {"ok": False, "error": str(exc)})
|
|
54
|
+
return
|
|
55
|
+
finally:
|
|
56
|
+
conn.close()
|
|
57
|
+
self._json(200, payload)
|
|
58
|
+
|
|
59
|
+
def _route_post_message_reactions_toggle(self, body: dict) -> None:
|
|
60
|
+
srv = _srv()
|
|
61
|
+
user_id = str(getattr(self, "auth_user", "") or "")
|
|
62
|
+
if not user_id:
|
|
63
|
+
self._json(401, {"ok": False, "error": "no_session"})
|
|
64
|
+
return
|
|
65
|
+
conn = srv._workframe_db()
|
|
66
|
+
try:
|
|
67
|
+
payload = message_reactions.toggle_reaction(
|
|
68
|
+
conn,
|
|
69
|
+
str(body.get("scope") or ""),
|
|
70
|
+
str(body.get("message_id") or ""),
|
|
71
|
+
str(body.get("emoji") or ""),
|
|
72
|
+
user_id,
|
|
73
|
+
srv._user_can_access_room,
|
|
74
|
+
)
|
|
75
|
+
except PermissionError:
|
|
76
|
+
self._json(403, {"ok": False, "error": "forbidden"})
|
|
77
|
+
return
|
|
78
|
+
except ValueError as exc:
|
|
79
|
+
self._json(400, {"ok": False, "error": str(exc)})
|
|
80
|
+
return
|
|
81
|
+
finally:
|
|
82
|
+
conn.close()
|
|
83
|
+
self._json(200, payload)
|
|
84
|
+
|
|
34
85
|
def _route_get_board(self, qs: dict[str, list[str]]) -> None:
|
|
35
86
|
srv = _srv()
|
|
36
87
|
self._json(200, srv.board_list())
|
|
@@ -452,8 +503,9 @@ class WorkspaceRoutesMixin:
|
|
|
452
503
|
return
|
|
453
504
|
sender_agent = str(body.get("sender_agent_id", "")).strip()
|
|
454
505
|
content = str(body.get("content", "")).strip()
|
|
455
|
-
content_type = str(body.get("content_type"
|
|
456
|
-
|
|
506
|
+
content_type = str(body.get("content_type") or "text").strip()
|
|
507
|
+
parent_value = body.get("parent_message_id")
|
|
508
|
+
parent_id = str(parent_value).strip() if parent_value else None
|
|
457
509
|
if not content:
|
|
458
510
|
self._json(400, {"error": "content required"})
|
|
459
511
|
return
|
|
@@ -875,6 +927,7 @@ class WorkspaceRoutesMixin:
|
|
|
875
927
|
self._json(400, {"error": str(exc)})
|
|
876
928
|
return
|
|
877
929
|
cred_id = str(payload["credential_id"])
|
|
930
|
+
srv._revoke_runtime_llm_leases(workspace_id=ws_id, provider=provider)
|
|
878
931
|
self._log_audit("credential_stored", "credential_binding", cred_id, f"provider={provider}")
|
|
879
932
|
try:
|
|
880
933
|
srv._bootstrap_model_after_llm_connect("", ws_id, provider)
|
|
@@ -923,6 +976,11 @@ class WorkspaceRoutesMixin:
|
|
|
923
976
|
if not affected:
|
|
924
977
|
self._json(404, {"error": "credential_not_found"})
|
|
925
978
|
return
|
|
979
|
+
srv._revoke_runtime_llm_leases(
|
|
980
|
+
workspace_id=ws_id,
|
|
981
|
+
provider=provider,
|
|
982
|
+
credential_binding_id=binding_id,
|
|
983
|
+
)
|
|
926
984
|
self._log_audit("credential_revoked", "credential_binding", binding_id, f"workspace={ws_id}")
|
|
927
985
|
spec = srv._catalog_provider(provider)
|
|
928
986
|
if spec and str(spec.get("category") or "") == "messaging":
|