pi-cloud 2.0.1 → 2.0.2
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/CHANGELOG.md +26 -2
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/client/dist/.vite/manifest.json +332 -308
- package/client/dist/assets/{EditorPanel-5KsKffCI.css → EditorPanel-CVtykOZb.css} +1 -1
- package/client/dist/assets/EditorPanel-CmUoE78w.js +968 -0
- package/client/dist/assets/{GitHistoryView-fz83ZRs_.js → GitHistoryView-v-2M8_wB.js} +1 -1
- package/client/dist/assets/{MemoryCenter-Di8khKRv.js → MemoryCenter-C7FlmiQa.js} +1 -1
- package/client/dist/assets/{PhCaretLeft.vue-C_mgOR0C.js → PhCaretLeft.vue-mR6H4iil.js} +1 -1
- package/client/dist/assets/PhFolderOpen.vue-CBZCIEKc.js +1 -0
- package/client/dist/assets/SettingsDialog-BaIuifOT.css +1 -0
- package/client/dist/assets/SettingsDialog-CmkVcpN6.js +5 -0
- package/client/dist/assets/TaskInboxPanel-CZiNlRVB.js +1 -0
- package/client/dist/assets/{TaskQueuePanel-Bnvgy4cm.css → TaskInboxPanel-D-F8nXji.css} +1 -1
- package/client/dist/assets/{TerminalPanel-CWNxgT84.js → TerminalPanel-BKnITvmW.js} +1 -1
- package/client/dist/assets/{abnfDiagram-VCTEODGH-o53BjVqy.js → abnfDiagram-VCTEODGH-C0onveOK.js} +1 -1
- package/client/dist/assets/{arc-DsZjnKYA.js → arc-BtnkAKcM.js} +1 -1
- package/client/dist/assets/{architectureDiagram-5GKGNRK7-u5Bv1Ym6.js → architectureDiagram-5GKGNRK7-BWzkt3mg.js} +1 -1
- package/client/dist/assets/{blockDiagram-NRAW4CY4-BSDeSfLw.js → blockDiagram-NRAW4CY4-B5U3jj5J.js} +1 -1
- package/client/dist/assets/{c4Diagram-UCG6FXSJ-B6NfOWtd.js → c4Diagram-UCG6FXSJ-BijI8bRn.js} +1 -1
- package/client/dist/assets/channel-Fc9PxOLB.js +1 -0
- package/client/dist/assets/{chunk-2Q5K7J3B-CgX2R8z-.js → chunk-2Q5K7J3B-BEieUt6C.js} +1 -1
- package/client/dist/assets/{chunk-5VM5RSS4-ha_fObnM.js → chunk-5VM5RSS4-DK306biR.js} +1 -1
- package/client/dist/assets/{chunk-F27PBJKO-DI1DZ-wc.js → chunk-F27PBJKO-jkwA9GYW.js} +1 -1
- package/client/dist/assets/{chunk-G27WJ6UU-Dg1UIr6p.js → chunk-G27WJ6UU-DnVXeFY7.js} +1 -1
- package/client/dist/assets/{chunk-JWPE2WC7-B3XeLm0J.js → chunk-JWPE2WC7-DpOSXvW8.js} +1 -1
- package/client/dist/assets/{chunk-LCL6LL3I-qz7b1c_I.js → chunk-LCL6LL3I-BD7QKy4w.js} +1 -1
- package/client/dist/assets/{chunk-POPQ4Y6H-B5NSvr4G.js → chunk-POPQ4Y6H-BqLcddFA.js} +1 -1
- package/client/dist/assets/{chunk-SVP7TREG-7QwKLsdb.js → chunk-SVP7TREG-C9GhrbFg.js} +1 -1
- package/client/dist/assets/{chunk-XXDRQBXY-CAosos5-.js → chunk-XXDRQBXY-jQoIqp5a.js} +1 -1
- package/client/dist/assets/classDiagram-DTDB5LWJ-D6WMV2bo.js +1 -0
- package/client/dist/assets/classDiagram-v2-JRS7N3AN-D6WMV2bo.js +1 -0
- package/client/dist/assets/{cose-bilkent-JH36ORCC-BsDQpeGC.js → cose-bilkent-JH36ORCC-DNt10hsx.js} +1 -1
- package/client/dist/assets/cssMode-D5VnGCRL.js +1 -0
- package/client/dist/assets/{cynefin-OW5HDTMX-Drahq6GQ.js → cynefin-OW5HDTMX-wI0wNPcp.js} +1 -1
- package/client/dist/assets/{cynefinDiagram-5FMLGOSQ-CnkS0oyb.js → cynefinDiagram-5FMLGOSQ-DwksvA8r.js} +1 -1
- package/client/dist/assets/{dagre-3AP2YEHR-BKQcTp5_.js → dagre-3AP2YEHR-ZY-MhFWR.js} +1 -1
- package/client/dist/assets/{diagram-S7CK7UJ4-CRJVo1Nm.js → diagram-S7CK7UJ4-BKaJVqZu.js} +1 -1
- package/client/dist/assets/{diagram-UQ7AKVKN-DsP-WSSg.js → diagram-UQ7AKVKN-DAaE-rA1.js} +1 -1
- package/client/dist/assets/{diagram-VSXAHHWV-Ccd1iqfn.js → diagram-VSXAHHWV-CJJ9xyap.js} +1 -1
- package/client/dist/assets/{diagram-VX7I27RA-D5w0ryS3.js → diagram-VX7I27RA-4Er142Er.js} +1 -1
- package/client/dist/assets/{diagram-Z3DM3KII-DpUTugp8.js → diagram-Z3DM3KII-eEe26_w-.js} +1 -1
- package/client/dist/assets/{ebnfDiagram-PWID7BFC-Donm234O.js → ebnfDiagram-PWID7BFC-Dshy_lHs.js} +1 -1
- package/client/dist/assets/{erDiagram-SSCWMZ5O-CvtobvjZ.js → erDiagram-SSCWMZ5O-k7LwDRbW.js} +1 -1
- package/client/dist/assets/{flowDiagram-A5DVABFB-v8UIYgnu.js → flowDiagram-A5DVABFB-7_X479bX.js} +1 -1
- package/client/dist/assets/{freemarker2-CCBoOScX.js → freemarker2-B7Gu__z_.js} +2 -2
- package/client/dist/assets/{ganttDiagram-EL5Y4UJY-TCK2YgJA.js → ganttDiagram-EL5Y4UJY-5WmBzvON.js} +1 -1
- package/client/dist/assets/{gitGraphDiagram-WWUBYQGX-BTVfzqSz.js → gitGraphDiagram-WWUBYQGX-CGTy36Lq.js} +1 -1
- package/client/dist/assets/handlebars-DRBRCJNd.js +1 -0
- package/client/dist/assets/html-6prUe0kc.js +1 -0
- package/client/dist/assets/htmlMode-DWoHKeAk.js +1 -0
- package/client/dist/assets/index-D8pylZiM.css +1 -0
- package/client/dist/assets/{index-B4PeEJ0f.js → index-GTyUzvDG.js} +1 -1
- package/client/dist/assets/index-nLzofEMh.js +407 -0
- package/client/dist/assets/{infoDiagram-RXCK75RN-BkzEFABK.js → infoDiagram-RXCK75RN-DZVb1P4d.js} +1 -1
- package/client/dist/assets/{ishikawaDiagram-5VMMS53U-DuVtEdHP.js → ishikawaDiagram-5VMMS53U-CMgYHU70.js} +1 -1
- package/client/dist/assets/javascript-Celvl7cV.js +1 -0
- package/client/dist/assets/{journeyDiagram-EYS64GPL-Cx5cuIcS.js → journeyDiagram-EYS64GPL-BnqHXNJS.js} +1 -1
- package/client/dist/assets/{jsonMode-BZHZxAtU.js → jsonMode-DLC0POfi.js} +2 -2
- package/client/dist/assets/{kanban-definition-3QL26DDD-wXihmZ_e.js → kanban-definition-3QL26DDD-BeStEVi-.js} +1 -1
- package/client/dist/assets/{layout-C2KDikn-.js → layout-B8XEB-tZ.js} +1 -1
- package/client/dist/assets/{linear-C6ueyh3n.js → linear-C1qdgmvA.js} +1 -1
- package/client/dist/assets/liquid-CiU5o5FX.js +1 -0
- package/client/dist/assets/{lspLanguageFeatures-C5jWIiYu.js → lspLanguageFeatures-BXNRwtlE.js} +1 -1
- package/client/dist/assets/mdx-CfTUs1eY.js +1 -0
- package/client/dist/assets/{mermaid.core-CcwuCXLB.js → mermaid.core-CFpSBIjf.js} +5 -5
- package/client/dist/assets/{mindmap-definition-FBJOCRG2-JL1NZYBY.js → mindmap-definition-FBJOCRG2-CUNP7NW4.js} +1 -1
- package/client/dist/assets/{pegDiagram-XKGWAZYB-DS69_m-N.js → pegDiagram-XKGWAZYB-bRD6LI7t.js} +1 -1
- package/client/dist/assets/{pieDiagram-E7YTZNPT-DkXpPONo.js → pieDiagram-E7YTZNPT-CUE2D9Mz.js} +1 -1
- package/client/dist/assets/python-rOgKTqjK.js +1 -0
- package/client/dist/assets/{quadrantDiagram-AXDQQJYC-DkkBvxWI.js → quadrantDiagram-AXDQQJYC-zDDE2XzB.js} +1 -1
- package/client/dist/assets/{railroadDiagram-O6MQD6OU-DCC05goQ.js → railroadDiagram-O6MQD6OU-3TRS2gLn.js} +1 -1
- package/client/dist/assets/razor-DhEzBZvo.js +1 -0
- package/client/dist/assets/{requirementDiagram-EFPCY7ZU-BwD-Jp5O.js → requirementDiagram-EFPCY7ZU-Cgm0MiBG.js} +1 -1
- package/client/dist/assets/{sankeyDiagram-P5KCCOFB-GlrDZlAs.js → sankeyDiagram-P5KCCOFB-DzQfpaqZ.js} +1 -1
- package/client/dist/assets/{sequenceDiagram-WJ2MYXX4-u5NKUnHb.js → sequenceDiagram-WJ2MYXX4-D_eKGGY5.js} +1 -1
- package/client/dist/assets/{sizeCapture-X5ZJPWSS-DWoXbO58.js → sizeCapture-X5ZJPWSS-BVFpPdEZ.js} +1 -1
- package/client/dist/assets/{stateDiagram-HBIQ2CUA-Bm1dFyez.js → stateDiagram-HBIQ2CUA-Bvp5OM6x.js} +1 -1
- package/client/dist/assets/stateDiagram-v2-4QOOHH4V-CM3P74Kw.js +1 -0
- package/client/dist/assets/{swimlanes-XN3QIQJK-DUNKblS5.js → swimlanes-XN3QIQJK-B497DW3A.js} +1 -1
- package/client/dist/assets/swimlanesDiagram-VK2B7HYN-DAMUXB40.js +8 -0
- package/client/dist/assets/{timeline-definition-24CTP7MA-toWxO6Z2.js → timeline-definition-24CTP7MA-BHAeybVL.js} +1 -1
- package/client/dist/assets/{tsMode-DZAgDpaL.js → tsMode-BKQ7PNBK.js} +2 -2
- package/client/dist/assets/typescript-0T3kV3Fw.js +1 -0
- package/client/dist/assets/{useTerminal-BcxidJuH.js → useTerminal-D0Eg4ohV.js} +1 -1
- package/client/dist/assets/{vennDiagram-4TSXK5OY-BkCayCPN.js → vennDiagram-4TSXK5OY-C3aoPRLD.js} +1 -1
- package/client/dist/assets/{wardleyDiagram-VM6X3IG4-BqKWO2eU.js → wardleyDiagram-VM6X3IG4-D45EDW-W.js} +1 -1
- package/client/dist/assets/{workers-CPOEug1v.js → workers-nJulRuKZ.js} +1 -1
- package/client/dist/assets/xml-Dm41fNV2.js +1 -0
- package/client/dist/assets/{xychartDiagram-S5SC5T6Z-BIAgMyo_.js → xychartDiagram-S5SC5T6Z-CCfr24LB.js} +1 -1
- package/client/dist/assets/yaml-BHs6-_Vy.js +1 -0
- package/client/dist/index.html +2 -2
- package/package.json +4 -3
- package/server/dist/index.js +1113 -382
- package/client/dist/assets/EditorPanel-jZgIBqB6.js +0 -968
- package/client/dist/assets/SettingsDialog-BsTvb8AX.js +0 -2
- package/client/dist/assets/SettingsDialog-JpBlJzlR.css +0 -1
- package/client/dist/assets/TaskQueuePanel-Bbk8CKW5.js +0 -1
- package/client/dist/assets/channel-vm9b-dk7.js +0 -1
- package/client/dist/assets/classDiagram-DTDB5LWJ-BZ5qRTrF.js +0 -1
- package/client/dist/assets/classDiagram-v2-JRS7N3AN-BZ5qRTrF.js +0 -1
- package/client/dist/assets/cssMode-Bixb-eY-.js +0 -1
- package/client/dist/assets/handlebars-D_iO3oKV.js +0 -1
- package/client/dist/assets/html-BxskKvwE.js +0 -1
- package/client/dist/assets/htmlMode-BRqzPJmn.js +0 -1
- package/client/dist/assets/index-2q9oNVFR.js +0 -407
- package/client/dist/assets/index-DBGNeJhr.css +0 -1
- package/client/dist/assets/javascript-DLM6N2IL.js +0 -1
- package/client/dist/assets/liquid-CK9kFoUD.js +0 -1
- package/client/dist/assets/mdx-DJw_6jAn.js +0 -1
- package/client/dist/assets/python-Z3BmknC5.js +0 -1
- package/client/dist/assets/razor-CQA_N38O.js +0 -1
- package/client/dist/assets/stateDiagram-v2-4QOOHH4V-DjQoyKBb.js +0 -1
- package/client/dist/assets/swimlanesDiagram-VK2B7HYN-7MB5gr5J.js +0 -8
- package/client/dist/assets/typescript-BGesFGk_.js +0 -1
- package/client/dist/assets/xml-BLpTRmDm.js +0 -1
- package/client/dist/assets/yaml-PqYnl7Hj.js +0 -1
package/server/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
14
14
|
|
|
15
15
|
// src/routes/sessions.ts
|
|
16
16
|
import * as os3 from "os";
|
|
17
|
-
import { dirname as dirname2 } from "path";
|
|
17
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2 } from "path";
|
|
18
18
|
|
|
19
19
|
// src/services/project-mover.ts
|
|
20
20
|
import { access as access2, rename, stat } from "fs/promises";
|
|
@@ -140,12 +140,12 @@ var ProjectMover = class {
|
|
|
140
140
|
}
|
|
141
141
|
return trimmed;
|
|
142
142
|
}
|
|
143
|
-
async assertDirectory(path13,
|
|
143
|
+
async assertDirectory(path13, message2) {
|
|
144
144
|
try {
|
|
145
145
|
const stats = await stat(path13);
|
|
146
|
-
if (!stats.isDirectory()) throw new Error(
|
|
146
|
+
if (!stats.isDirectory()) throw new Error(message2);
|
|
147
147
|
} catch (error) {
|
|
148
|
-
if (isMissingPathError(error)) throw new Error(
|
|
148
|
+
if (isMissingPathError(error)) throw new Error(message2);
|
|
149
149
|
throw error;
|
|
150
150
|
}
|
|
151
151
|
}
|
|
@@ -775,10 +775,12 @@ async function sessionRoutes(app, options = {}) {
|
|
|
775
775
|
if (!profileId) return reply.status(400).send({ error: "profileId is required" });
|
|
776
776
|
const owner = { type: "profile", id: profileId };
|
|
777
777
|
const idsByGroup = options.pinStore.listSessionIdsByGroup(owner);
|
|
778
|
+
const filesByGroup = options.pinStore.listFilePathsByGroup(profileId);
|
|
778
779
|
return {
|
|
779
780
|
groups: options.pinStore.listGroups(owner).map((group) => ({
|
|
780
781
|
...group,
|
|
781
|
-
sessionIds: idsByGroup.get(group.id) || []
|
|
782
|
+
sessionIds: idsByGroup.get(group.id) || [],
|
|
783
|
+
filePaths: filesByGroup.get(group.id) || []
|
|
782
784
|
}))
|
|
783
785
|
};
|
|
784
786
|
});
|
|
@@ -792,6 +794,26 @@ async function sessionRoutes(app, options = {}) {
|
|
|
792
794
|
return reply.status(400).send({ error: error instanceof Error ? error.message : "Failed to create pin group" });
|
|
793
795
|
}
|
|
794
796
|
});
|
|
797
|
+
app.put("/files/pin", async (req, reply) => {
|
|
798
|
+
if (!options.pinStore) return reply.status(503).send({ error: "Session pins are not configured" });
|
|
799
|
+
const { filePath, groupId, profileId } = req.body;
|
|
800
|
+
if (!filePath || !isAbsolute2(filePath) || !groupId || !profileId) {
|
|
801
|
+
return reply.status(400).send({ error: "absolute filePath, groupId, and profileId are required" });
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
options.pinStore.pinFile(profileId, filePath, groupId);
|
|
805
|
+
return { success: true };
|
|
806
|
+
} catch (error) {
|
|
807
|
+
return reply.status(404).send({ error: error instanceof Error ? error.message : "Pin group not found" });
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
app.delete("/files/pin", async (req, reply) => {
|
|
811
|
+
if (!options.pinStore) return reply.status(503).send({ error: "Session pins are not configured" });
|
|
812
|
+
const { filePath, profileId } = req.query;
|
|
813
|
+
if (!filePath || !profileId) return reply.status(400).send({ error: "filePath and profileId are required" });
|
|
814
|
+
options.pinStore.unpinFile(profileId, filePath);
|
|
815
|
+
return { success: true };
|
|
816
|
+
});
|
|
795
817
|
app.put("/:id/pin", async (req, reply) => {
|
|
796
818
|
if (!options.pinStore) return reply.status(503).send({ error: "Session pins are not configured" });
|
|
797
819
|
const { id } = req.params;
|
|
@@ -823,9 +845,11 @@ async function sessionRoutes(app, options = {}) {
|
|
|
823
845
|
}, worktreeMetadata)]));
|
|
824
846
|
const owner = { type: "profile", id: profileId };
|
|
825
847
|
const idsByGroup = options.pinStore.listSessionIdsByGroup(owner);
|
|
848
|
+
const filesByGroup = options.pinStore.listFilePathsByGroup(profileId);
|
|
826
849
|
return {
|
|
827
850
|
groups: options.pinStore.listGroups(owner).map((group) => ({
|
|
828
851
|
...group,
|
|
852
|
+
filePaths: filesByGroup.get(group.id) || [],
|
|
829
853
|
sessions: (idsByGroup.get(group.id) || []).map((id) => sessionsById.get(id)).filter((session) => Boolean(session)).map(toSessionListItem)
|
|
830
854
|
}))
|
|
831
855
|
};
|
|
@@ -1054,9 +1078,9 @@ async function sessionRoutes(app, options = {}) {
|
|
|
1054
1078
|
skippedSessionFiles: sessionResult.skipped
|
|
1055
1079
|
};
|
|
1056
1080
|
} catch (error) {
|
|
1057
|
-
const
|
|
1058
|
-
const status =
|
|
1059
|
-
return reply.status(status).send({ error:
|
|
1081
|
+
const message2 = error instanceof Error ? error.message : "Failed to move project";
|
|
1082
|
+
const status = message2.includes("already exists") || message2.includes("streaming") ? 409 : 400;
|
|
1083
|
+
return reply.status(status).send({ error: message2 });
|
|
1060
1084
|
}
|
|
1061
1085
|
});
|
|
1062
1086
|
app.post("/relocate-project", async (req, reply) => {
|
|
@@ -1084,8 +1108,8 @@ async function sessionRoutes(app, options = {}) {
|
|
|
1084
1108
|
sessionService.invalidateSessionListCache();
|
|
1085
1109
|
return { success: true, moved: result.moved, skipped: result.skipped };
|
|
1086
1110
|
} catch (error) {
|
|
1087
|
-
const
|
|
1088
|
-
return reply.status(
|
|
1111
|
+
const message2 = error instanceof Error ? error.message : "Failed to relocate project sessions";
|
|
1112
|
+
return reply.status(message2.includes("already exists") ? 409 : 400).send({ error: message2 });
|
|
1089
1113
|
}
|
|
1090
1114
|
});
|
|
1091
1115
|
app.post("/:id/relocate", async (req, reply) => {
|
|
@@ -1121,8 +1145,8 @@ async function sessionRoutes(app, options = {}) {
|
|
|
1121
1145
|
sessionService.invalidateSessionListCache();
|
|
1122
1146
|
return { success: true, path: result.destinationPath, cwd: newCwd };
|
|
1123
1147
|
} catch (error) {
|
|
1124
|
-
const
|
|
1125
|
-
return reply.status(
|
|
1148
|
+
const message2 = error instanceof Error ? error.message : "Failed to move session";
|
|
1149
|
+
return reply.status(message2.includes("already exists") ? 409 : 400).send({ error: message2 });
|
|
1126
1150
|
}
|
|
1127
1151
|
});
|
|
1128
1152
|
app.post("/:id/resume", async (req) => {
|
|
@@ -1179,8 +1203,8 @@ async function sessionRoutes(app, options = {}) {
|
|
|
1179
1203
|
try {
|
|
1180
1204
|
return await sessionService.updateSessionSkillPolicy(clientId, id, mode, skills || []);
|
|
1181
1205
|
} catch (error) {
|
|
1182
|
-
const
|
|
1183
|
-
return reply.status(
|
|
1206
|
+
const message2 = error instanceof Error ? error.message : "Failed to update session skills";
|
|
1207
|
+
return reply.status(message2.includes("streaming") ? 409 : 404).send({ error: message2 });
|
|
1184
1208
|
}
|
|
1185
1209
|
});
|
|
1186
1210
|
app.post("/:id/model", async (req, reply) => {
|
|
@@ -1192,8 +1216,8 @@ async function sessionRoutes(app, options = {}) {
|
|
|
1192
1216
|
try {
|
|
1193
1217
|
return await sessionService.setSessionModel(clientId, id, provider, modelId);
|
|
1194
1218
|
} catch (error) {
|
|
1195
|
-
const
|
|
1196
|
-
return reply.status(
|
|
1219
|
+
const message2 = error instanceof Error ? error.message : "Failed to change model";
|
|
1220
|
+
return reply.status(message2.includes("streaming") ? 409 : 400).send({ error: message2 });
|
|
1197
1221
|
}
|
|
1198
1222
|
});
|
|
1199
1223
|
app.post("/:id/thinking-level", async (req, reply) => {
|
|
@@ -1205,8 +1229,8 @@ async function sessionRoutes(app, options = {}) {
|
|
|
1205
1229
|
try {
|
|
1206
1230
|
return await sessionService.setSessionThinkingLevel(clientId, id, level);
|
|
1207
1231
|
} catch (error) {
|
|
1208
|
-
const
|
|
1209
|
-
return reply.status(
|
|
1232
|
+
const message2 = error instanceof Error ? error.message : "Failed to change thinking level";
|
|
1233
|
+
return reply.status(message2.includes("streaming") ? 409 : 400).send({ error: message2 });
|
|
1210
1234
|
}
|
|
1211
1235
|
});
|
|
1212
1236
|
app.get("/:id/summary", async (req, reply) => {
|
|
@@ -1330,6 +1354,7 @@ async function sessionRoutes(app, options = {}) {
|
|
|
1330
1354
|
model: foundSession.model,
|
|
1331
1355
|
thinkingLevel: foundSession.thinkingLevel,
|
|
1332
1356
|
isStreaming: foundSession.isStreaming,
|
|
1357
|
+
streamingStartedAt: sessionService.getSessionStreamingStartedAt(foundSession.sessionId),
|
|
1333
1358
|
activity: options.activityStore?.listForSession(foundSession.sessionId) || []
|
|
1334
1359
|
}, worktreeMetadata);
|
|
1335
1360
|
}
|
|
@@ -1406,8 +1431,8 @@ async function sessionRoutes(app, options = {}) {
|
|
|
1406
1431
|
const result = await sessionService.renameSession(clientId, id, name);
|
|
1407
1432
|
return result;
|
|
1408
1433
|
} catch (error) {
|
|
1409
|
-
const
|
|
1410
|
-
return reply.status(
|
|
1434
|
+
const message2 = error instanceof Error ? error.message : "Failed to rename session";
|
|
1435
|
+
return reply.status(message2.includes("not found") ? 404 : 400).send({ error: message2 });
|
|
1411
1436
|
}
|
|
1412
1437
|
});
|
|
1413
1438
|
app.delete("/:id", async (req, reply) => {
|
|
@@ -1440,8 +1465,8 @@ var MEMORY_CATEGORIES = ["rule", "preference", "decision", "fact", "pitfall"];
|
|
|
1440
1465
|
var MEMORY_STATUSES = ["active", "pending", "archived"];
|
|
1441
1466
|
var PINNED_APPLICABILITY_MODES = ["always", "matched"];
|
|
1442
1467
|
var MemoryRouteError = class extends Error {
|
|
1443
|
-
constructor(statusCode,
|
|
1444
|
-
super(
|
|
1468
|
+
constructor(statusCode, message2) {
|
|
1469
|
+
super(message2);
|
|
1445
1470
|
this.statusCode = statusCode;
|
|
1446
1471
|
}
|
|
1447
1472
|
statusCode;
|
|
@@ -1561,7 +1586,10 @@ async function memoryRoutes(app) {
|
|
|
1561
1586
|
app.post("/extractions/:id/clear", async (req, reply) => {
|
|
1562
1587
|
try {
|
|
1563
1588
|
const body = requestBody(req.body);
|
|
1564
|
-
const context = await resolveContext(app,
|
|
1589
|
+
const context = await resolveContext(app, {
|
|
1590
|
+
clientId: body.clientId,
|
|
1591
|
+
projectPath: body.projectPath
|
|
1592
|
+
});
|
|
1565
1593
|
const { id } = req.params;
|
|
1566
1594
|
requireAccessibleRun(app, context, id);
|
|
1567
1595
|
app.memoryRuntime.store.clearFailedRun(id);
|
|
@@ -1767,13 +1795,13 @@ function clampNumber(value, min, max, fallback) {
|
|
|
1767
1795
|
return Math.min(max, Math.max(min, Math.trunc(parsed)));
|
|
1768
1796
|
}
|
|
1769
1797
|
function sendError(reply, error) {
|
|
1770
|
-
const
|
|
1771
|
-
if (error instanceof MemoryRouteError) return reply.status(error.statusCode).send({ error:
|
|
1772
|
-
if (/not found/i.test(
|
|
1773
|
-
if (/modified|already exists|not pending|not failed|not completed|cannot be requeued|superseded/i.test(
|
|
1774
|
-
return reply.status(409).send({ error:
|
|
1798
|
+
const message2 = error instanceof Error ? error.message : "Memory request failed";
|
|
1799
|
+
if (error instanceof MemoryRouteError) return reply.status(error.statusCode).send({ error: message2 });
|
|
1800
|
+
if (/not found/i.test(message2)) return reply.status(404).send({ error: message2 });
|
|
1801
|
+
if (/modified|already exists|not pending|not failed|not completed|cannot be requeued|superseded/i.test(message2)) {
|
|
1802
|
+
return reply.status(409).send({ error: message2 });
|
|
1775
1803
|
}
|
|
1776
|
-
return reply.status(400).send({ error:
|
|
1804
|
+
return reply.status(400).send({ error: message2 });
|
|
1777
1805
|
}
|
|
1778
1806
|
|
|
1779
1807
|
// src/routes/files.ts
|
|
@@ -2186,9 +2214,9 @@ async function fileRoutes(app) {
|
|
|
2186
2214
|
mtime: stats.mtimeMs
|
|
2187
2215
|
};
|
|
2188
2216
|
} catch (error) {
|
|
2189
|
-
const
|
|
2217
|
+
const message2 = error instanceof Error ? error.message : "Unknown archive error";
|
|
2190
2218
|
return reply.code(415).send({
|
|
2191
|
-
error: `Archive preview failed: ${
|
|
2219
|
+
error: `Archive preview failed: ${message2}`,
|
|
2192
2220
|
kind: "binary",
|
|
2193
2221
|
path: resolvedPath,
|
|
2194
2222
|
mtime: stats.mtimeMs
|
|
@@ -2929,12 +2957,12 @@ async function synthesizeSpeech(text, options = {}) {
|
|
|
2929
2957
|
continue;
|
|
2930
2958
|
}
|
|
2931
2959
|
if (!response.ok) {
|
|
2932
|
-
const
|
|
2960
|
+
const message2 = await providerError(response);
|
|
2933
2961
|
if (isRetryableStatus2(response.status) && attempt < MAX_SYNTHESIS_ATTEMPTS) {
|
|
2934
2962
|
await wait2(retryDelayMs * attempt);
|
|
2935
2963
|
continue;
|
|
2936
2964
|
}
|
|
2937
|
-
throw new Error(
|
|
2965
|
+
throw new Error(message2);
|
|
2938
2966
|
}
|
|
2939
2967
|
const audio = Buffer.from(await response.arrayBuffer());
|
|
2940
2968
|
if (audio.length === 0) throw new Error("The text-to-speech provider returned an empty response.");
|
|
@@ -3202,8 +3230,8 @@ function parseBranchName(name, files) {
|
|
|
3202
3230
|
if (!files.length) throw new Error("No git changes to suggest a branch name from");
|
|
3203
3231
|
return sanitizeBranchName(proposeCommitMessage(files));
|
|
3204
3232
|
}
|
|
3205
|
-
function textFromAssistantMessage(
|
|
3206
|
-
return
|
|
3233
|
+
function textFromAssistantMessage(message2) {
|
|
3234
|
+
return message2.content.filter((item) => item.type === "text").map((item) => item.text).join("\n").trim();
|
|
3207
3235
|
}
|
|
3208
3236
|
function joinGitOutput(...parts) {
|
|
3209
3237
|
return parts.map((part) => part.replace(/^(?:\r?\n)+|(?:\r?\n)+$/g, "")).filter((part) => part.trim()).join("\n\n");
|
|
@@ -3365,9 +3393,9 @@ async function generateCommitMessageWithAi(sessionService, clientId, cwd, status
|
|
|
3365
3393
|
if (response.stopReason === "error") {
|
|
3366
3394
|
throw new Error(response.errorMessage || "AI commit message generation failed");
|
|
3367
3395
|
}
|
|
3368
|
-
const
|
|
3369
|
-
if (!
|
|
3370
|
-
return
|
|
3396
|
+
const message2 = textFromAssistantMessage(response).replace(/^```(?:text)?/i, "").replace(/```$/i, "").trim().replace(/^['"]|['"]$/g, "").trim();
|
|
3397
|
+
if (!message2) throw new Error("AI did not return a commit message");
|
|
3398
|
+
return message2;
|
|
3371
3399
|
}
|
|
3372
3400
|
function parseChangedRanges(diff) {
|
|
3373
3401
|
const changes = {};
|
|
@@ -3442,7 +3470,7 @@ async function gitRoutes(app, options = {}) {
|
|
|
3442
3470
|
}) || { global: {}, project: {}, effective: DEFAULT_COMMIT_MESSAGE_PROMPTS };
|
|
3443
3471
|
});
|
|
3444
3472
|
app.get("/status", async (req, reply) => {
|
|
3445
|
-
const { cwd, message, stagedOnly } = req.query;
|
|
3473
|
+
const { cwd, message: message2, stagedOnly } = req.query;
|
|
3446
3474
|
const resolvedCwd = await resolveGitCwd(cwd);
|
|
3447
3475
|
try {
|
|
3448
3476
|
if (!await isGitRepository(resolvedCwd)) {
|
|
@@ -3457,7 +3485,7 @@ async function gitRoutes(app, options = {}) {
|
|
|
3457
3485
|
cwd: resolvedCwd,
|
|
3458
3486
|
isRepository: true,
|
|
3459
3487
|
files,
|
|
3460
|
-
message:
|
|
3488
|
+
message: message2?.trim() || (files.length ? proposeCommitMessage(files) : ""),
|
|
3461
3489
|
output
|
|
3462
3490
|
};
|
|
3463
3491
|
} catch (error) {
|
|
@@ -3494,8 +3522,8 @@ async function gitRoutes(app, options = {}) {
|
|
|
3494
3522
|
commits: commits.slice(0, GIT_HISTORY_PAGE_SIZE)
|
|
3495
3523
|
};
|
|
3496
3524
|
} catch (error) {
|
|
3497
|
-
const
|
|
3498
|
-
return reply.status(400).send({ error:
|
|
3525
|
+
const message2 = error instanceof Error ? error.message : "Failed to load git history";
|
|
3526
|
+
return reply.status(400).send({ error: message2 });
|
|
3499
3527
|
}
|
|
3500
3528
|
});
|
|
3501
3529
|
app.get("/branches", async (req, reply) => {
|
|
@@ -3556,8 +3584,8 @@ async function gitRoutes(app, options = {}) {
|
|
|
3556
3584
|
return reply.status(400).send({ error: "No git changes to generate a commit message from" });
|
|
3557
3585
|
}
|
|
3558
3586
|
const prompts = options.commitMessagePrompts?.get(resolvedCwd).effective || DEFAULT_COMMIT_MESSAGE_PROMPTS;
|
|
3559
|
-
const
|
|
3560
|
-
return { cwd: resolvedCwd, message, files };
|
|
3587
|
+
const message2 = await generateCommitMessageWithAi(app.services.sessions, clientId, resolvedCwd, status, diff, prompts);
|
|
3588
|
+
return { cwd: resolvedCwd, message: message2, files };
|
|
3561
3589
|
} catch (error) {
|
|
3562
3590
|
const errorMessage2 = error instanceof Error ? error.message : "Failed to generate commit message";
|
|
3563
3591
|
return reply.status(400).send({ error: errorMessage2 });
|
|
@@ -3636,8 +3664,8 @@ async function gitRoutes(app, options = {}) {
|
|
|
3636
3664
|
app.post("/commit", async (req, reply) => {
|
|
3637
3665
|
const body = req.body || {};
|
|
3638
3666
|
const resolvedCwd = await resolveGitCwd(body.cwd);
|
|
3639
|
-
const
|
|
3640
|
-
if (!
|
|
3667
|
+
const message2 = body.message?.trim();
|
|
3668
|
+
if (!message2) {
|
|
3641
3669
|
return reply.status(400).send({ error: "Commit message is required" });
|
|
3642
3670
|
}
|
|
3643
3671
|
try {
|
|
@@ -3648,12 +3676,12 @@ async function gitRoutes(app, options = {}) {
|
|
|
3648
3676
|
if (!files.length) {
|
|
3649
3677
|
return reply.status(400).send({ error: "No changes to commit" });
|
|
3650
3678
|
}
|
|
3651
|
-
const output = await runGit(resolvedCwd, ["commit", "-m",
|
|
3679
|
+
const output = await runGit(resolvedCwd, ["commit", "-m", message2]);
|
|
3652
3680
|
const commit = (await runGit(resolvedCwd, ["rev-parse", "HEAD"])).trim();
|
|
3653
|
-
recordCommitActivity(options, { sessionId: body.sessionId, cwd: resolvedCwd, message, commit, files, mode: "commit" });
|
|
3681
|
+
recordCommitActivity(options, { sessionId: body.sessionId, cwd: resolvedCwd, message: message2, commit, files, mode: "commit" });
|
|
3654
3682
|
return {
|
|
3655
3683
|
cwd: resolvedCwd,
|
|
3656
|
-
message,
|
|
3684
|
+
message: message2,
|
|
3657
3685
|
files,
|
|
3658
3686
|
commit,
|
|
3659
3687
|
output
|
|
@@ -3664,7 +3692,7 @@ async function gitRoutes(app, options = {}) {
|
|
|
3664
3692
|
}
|
|
3665
3693
|
});
|
|
3666
3694
|
app.get("/amend-status", async (req, reply) => {
|
|
3667
|
-
const { cwd, message } = req.query;
|
|
3695
|
+
const { cwd, message: message2 } = req.query;
|
|
3668
3696
|
const resolvedCwd = await resolveGitCwd(cwd);
|
|
3669
3697
|
try {
|
|
3670
3698
|
await ensureHasCommit(resolvedCwd);
|
|
@@ -3675,7 +3703,7 @@ async function gitRoutes(app, options = {}) {
|
|
|
3675
3703
|
return {
|
|
3676
3704
|
cwd: resolvedCwd,
|
|
3677
3705
|
files: parseStatusFiles(status),
|
|
3678
|
-
message:
|
|
3706
|
+
message: message2?.trim() || previousMessage.trim()
|
|
3679
3707
|
};
|
|
3680
3708
|
} catch (error) {
|
|
3681
3709
|
const errorMessage2 = error instanceof Error ? error.message : "Failed to prepare git amend";
|
|
@@ -3685,8 +3713,8 @@ async function gitRoutes(app, options = {}) {
|
|
|
3685
3713
|
app.post("/amend", async (req, reply) => {
|
|
3686
3714
|
const body = req.body || {};
|
|
3687
3715
|
const resolvedCwd = await resolveGitCwd(body.cwd);
|
|
3688
|
-
const
|
|
3689
|
-
if (!
|
|
3716
|
+
const message2 = body.message?.trim();
|
|
3717
|
+
if (!message2) {
|
|
3690
3718
|
return reply.status(400).send({ error: "Commit message is required" });
|
|
3691
3719
|
}
|
|
3692
3720
|
try {
|
|
@@ -3694,12 +3722,12 @@ async function gitRoutes(app, options = {}) {
|
|
|
3694
3722
|
await runGit(resolvedCwd, ["add", "-A"]);
|
|
3695
3723
|
const status = await runGit(resolvedCwd, ["status", "--porcelain"]);
|
|
3696
3724
|
const files = parseStatusFiles(status);
|
|
3697
|
-
const output = await runGit(resolvedCwd, ["commit", "--amend", "--allow-empty", "-m",
|
|
3725
|
+
const output = await runGit(resolvedCwd, ["commit", "--amend", "--allow-empty", "-m", message2]);
|
|
3698
3726
|
const commit = (await runGit(resolvedCwd, ["rev-parse", "HEAD"])).trim();
|
|
3699
|
-
recordCommitActivity(options, { sessionId: body.sessionId, cwd: resolvedCwd, message, commit, files, mode: "amend" });
|
|
3727
|
+
recordCommitActivity(options, { sessionId: body.sessionId, cwd: resolvedCwd, message: message2, commit, files, mode: "amend" });
|
|
3700
3728
|
return {
|
|
3701
3729
|
cwd: resolvedCwd,
|
|
3702
|
-
message,
|
|
3730
|
+
message: message2,
|
|
3703
3731
|
files,
|
|
3704
3732
|
commit,
|
|
3705
3733
|
output
|
|
@@ -3719,8 +3747,8 @@ async function gitRoutes(app, options = {}) {
|
|
|
3719
3747
|
changes: parseChangedRanges(diff)
|
|
3720
3748
|
};
|
|
3721
3749
|
} catch (error) {
|
|
3722
|
-
const
|
|
3723
|
-
return reply.status(400).send({ error:
|
|
3750
|
+
const message2 = error instanceof Error ? error.message : "Failed to run git diff";
|
|
3751
|
+
return reply.status(400).send({ error: message2 });
|
|
3724
3752
|
}
|
|
3725
3753
|
});
|
|
3726
3754
|
app.get("/diff", async (req, reply) => {
|
|
@@ -3731,8 +3759,8 @@ async function gitRoutes(app, options = {}) {
|
|
|
3731
3759
|
if (commit) {
|
|
3732
3760
|
const diff2 = await runGit(resolvedCwd, ["show", "--format=", "--patch", commit]);
|
|
3733
3761
|
const remainingBytes2 = MAX_SLASH_COMMAND_OUTPUT_BYTES - Buffer.byteLength(diff2);
|
|
3734
|
-
const
|
|
3735
|
-
return { cwd: resolvedCwd, scope: `commit-${commit}`, commit, stat:
|
|
3762
|
+
const stat11 = remainingBytes2 > 0 ? await runGit(resolvedCwd, ["show", "--format=", "--stat", commit], remainingBytes2) : "";
|
|
3763
|
+
return { cwd: resolvedCwd, scope: `commit-${commit}`, commit, stat: stat11, diff: diff2 };
|
|
3736
3764
|
}
|
|
3737
3765
|
const scope = parseDiffScope(rawScope);
|
|
3738
3766
|
const pathArgs = path13 ? ["--", path13] : [];
|
|
@@ -3741,11 +3769,11 @@ async function gitRoutes(app, options = {}) {
|
|
|
3741
3769
|
diff = await appendUntrackedDiff(resolvedCwd, diff, [], path13, MAX_SLASH_COMMAND_OUTPUT_BYTES);
|
|
3742
3770
|
}
|
|
3743
3771
|
const remainingBytes = MAX_SLASH_COMMAND_OUTPUT_BYTES - Buffer.byteLength(diff);
|
|
3744
|
-
let
|
|
3772
|
+
let stat10 = remainingBytes > 0 ? await getDiff(resolvedCwd, ["--stat", ...pathArgs], scope, remainingBytes) : "";
|
|
3745
3773
|
if (includeUntracked === "true" && scope !== "staged" && remainingBytes > 0) {
|
|
3746
|
-
|
|
3774
|
+
stat10 = await appendUntrackedDiff(resolvedCwd, stat10, ["--stat"], path13, remainingBytes);
|
|
3747
3775
|
}
|
|
3748
|
-
return { cwd: resolvedCwd, scope, path: path13, stat:
|
|
3776
|
+
return { cwd: resolvedCwd, scope, path: path13, stat: stat10, diff };
|
|
3749
3777
|
} catch (error) {
|
|
3750
3778
|
if (error instanceof OversizedGitOutputError) {
|
|
3751
3779
|
return {
|
|
@@ -3756,8 +3784,8 @@ async function gitRoutes(app, options = {}) {
|
|
|
3756
3784
|
message: error.message
|
|
3757
3785
|
};
|
|
3758
3786
|
}
|
|
3759
|
-
const
|
|
3760
|
-
return reply.status(400).send({ error:
|
|
3787
|
+
const message2 = error instanceof Error ? error.message : "Failed to run git diff";
|
|
3788
|
+
return reply.status(400).send({ error: message2 });
|
|
3761
3789
|
}
|
|
3762
3790
|
});
|
|
3763
3791
|
}
|
|
@@ -3935,10 +3963,10 @@ var ProjectTaskStore = class {
|
|
|
3935
3963
|
if (!task) throw new ProjectTaskNotFoundError("Task not found");
|
|
3936
3964
|
return task;
|
|
3937
3965
|
}
|
|
3938
|
-
assertChanged(id, changes,
|
|
3966
|
+
assertChanged(id, changes, message2) {
|
|
3939
3967
|
if (changes) return;
|
|
3940
3968
|
if (!this.get(id)) throw new ProjectTaskNotFoundError("Task not found");
|
|
3941
|
-
throw new ProjectTaskConflictError(
|
|
3969
|
+
throw new ProjectTaskConflictError(message2);
|
|
3942
3970
|
}
|
|
3943
3971
|
};
|
|
3944
3972
|
function isProjectTaskStatus(value) {
|
|
@@ -4108,8 +4136,8 @@ async function taskRoutes(app, options) {
|
|
|
4108
4136
|
}
|
|
4109
4137
|
});
|
|
4110
4138
|
}
|
|
4111
|
-
function textFromAssistantMessage2(
|
|
4112
|
-
return
|
|
4139
|
+
function textFromAssistantMessage2(message2) {
|
|
4140
|
+
return message2.content.filter((item) => item.type === "text").map((item) => item.text).join("\n").trim();
|
|
4113
4141
|
}
|
|
4114
4142
|
function taskPolishPrompt(title, prompt) {
|
|
4115
4143
|
return `Polish this project task so it is ready for an AI coding agent to execute.
|
|
@@ -4202,11 +4230,11 @@ function optionalString3(value, field) {
|
|
|
4202
4230
|
return value.trim() || null;
|
|
4203
4231
|
}
|
|
4204
4232
|
function sendTaskError(reply, error, validationStatus = 409) {
|
|
4205
|
-
const
|
|
4206
|
-
if (error instanceof ProjectTaskNotFoundError) return reply.status(404).send({ error:
|
|
4207
|
-
if (error instanceof ProjectTaskConflictError) return reply.status(409).send({ error:
|
|
4208
|
-
if (error instanceof ProjectTaskValidationError) return reply.status(validationStatus).send({ error:
|
|
4209
|
-
return reply.status(500).send({ error:
|
|
4233
|
+
const message2 = error instanceof Error ? error.message : "Task request failed";
|
|
4234
|
+
if (error instanceof ProjectTaskNotFoundError) return reply.status(404).send({ error: message2 });
|
|
4235
|
+
if (error instanceof ProjectTaskConflictError) return reply.status(409).send({ error: message2 });
|
|
4236
|
+
if (error instanceof ProjectTaskValidationError) return reply.status(validationStatus).send({ error: message2 });
|
|
4237
|
+
return reply.status(500).send({ error: message2 });
|
|
4210
4238
|
}
|
|
4211
4239
|
|
|
4212
4240
|
// src/routes/feishu-gateway.ts
|
|
@@ -4222,8 +4250,8 @@ async function feishuGatewayRoutes(app, options) {
|
|
|
4222
4250
|
function sendFeishuGatewayError(reply, error) {
|
|
4223
4251
|
const maybeStatusCode = error?.statusCode;
|
|
4224
4252
|
const statusCode = typeof maybeStatusCode === "number" ? maybeStatusCode : 500;
|
|
4225
|
-
const
|
|
4226
|
-
return reply.status(statusCode).send({ error:
|
|
4253
|
+
const message2 = error instanceof Error ? error.message : "Feishu gateway request failed";
|
|
4254
|
+
return reply.status(statusCode).send({ error: message2 });
|
|
4227
4255
|
}
|
|
4228
4256
|
|
|
4229
4257
|
// src/routes/gateways.ts
|
|
@@ -4258,8 +4286,8 @@ async function gatewayRoutes(app, options) {
|
|
|
4258
4286
|
});
|
|
4259
4287
|
}
|
|
4260
4288
|
function sendGatewayError(reply, error) {
|
|
4261
|
-
const
|
|
4262
|
-
return reply.status(400).send({ error:
|
|
4289
|
+
const message2 = error instanceof Error ? error.message : "Gateway settings request failed";
|
|
4290
|
+
return reply.status(400).send({ error: message2 });
|
|
4263
4291
|
}
|
|
4264
4292
|
|
|
4265
4293
|
// src/routes/wecom-gateway.ts
|
|
@@ -4317,8 +4345,8 @@ async function wecomGatewayRoutes(app, options) {
|
|
|
4317
4345
|
}
|
|
4318
4346
|
function sendWecomError(reply, error) {
|
|
4319
4347
|
const statusCode = Number(error?.statusCode) || 400;
|
|
4320
|
-
const
|
|
4321
|
-
return reply.status(statusCode).send({ error:
|
|
4348
|
+
const message2 = error instanceof Error ? error.message : "WeCom gateway request failed";
|
|
4349
|
+
return reply.status(statusCode).send({ error: message2 });
|
|
4322
4350
|
}
|
|
4323
4351
|
|
|
4324
4352
|
// src/routes/git-hosting.ts
|
|
@@ -4360,15 +4388,15 @@ var GiteaSettingsStore = class {
|
|
|
4360
4388
|
return this.get();
|
|
4361
4389
|
}
|
|
4362
4390
|
clear() {
|
|
4363
|
-
this.db.prepare("DELETE FROM
|
|
4391
|
+
this.db.prepare("DELETE FROM application_settings WHERE key IN (?, ?)").run(SERVER_URL_KEY, TOKEN_KEY);
|
|
4364
4392
|
}
|
|
4365
4393
|
value(key) {
|
|
4366
|
-
const row = this.db.prepare("SELECT value FROM
|
|
4394
|
+
const row = this.db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
|
|
4367
4395
|
return row?.value;
|
|
4368
4396
|
}
|
|
4369
4397
|
set(key, value) {
|
|
4370
4398
|
this.db.prepare(`
|
|
4371
|
-
INSERT INTO
|
|
4399
|
+
INSERT INTO application_settings (key, value, updated_at)
|
|
4372
4400
|
VALUES (?, ?, ?)
|
|
4373
4401
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
4374
4402
|
`).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -4423,7 +4451,7 @@ var GithubSettingsStore = class {
|
|
|
4423
4451
|
const value = proxyUrl.trim();
|
|
4424
4452
|
if (value) new URL(value);
|
|
4425
4453
|
if (value) this.set(PROXY_URL_KEY, value);
|
|
4426
|
-
else this.db.prepare("DELETE FROM
|
|
4454
|
+
else this.db.prepare("DELETE FROM application_settings WHERE key = ?").run(PROXY_URL_KEY);
|
|
4427
4455
|
return this.get();
|
|
4428
4456
|
}
|
|
4429
4457
|
proxyEnv() {
|
|
@@ -4431,15 +4459,15 @@ var GithubSettingsStore = class {
|
|
|
4431
4459
|
return proxyUrl ? githubProxyEnvFromUrl(proxyUrl) : {};
|
|
4432
4460
|
}
|
|
4433
4461
|
clear() {
|
|
4434
|
-
this.db.prepare("DELETE FROM
|
|
4462
|
+
this.db.prepare("DELETE FROM application_settings WHERE key IN (?, ?)").run(SERVER_URL_KEY2, TOKEN_KEY2);
|
|
4435
4463
|
}
|
|
4436
4464
|
value(key) {
|
|
4437
|
-
const row = this.db.prepare("SELECT value FROM
|
|
4465
|
+
const row = this.db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
|
|
4438
4466
|
return row?.value;
|
|
4439
4467
|
}
|
|
4440
4468
|
set(key, value) {
|
|
4441
4469
|
this.db.prepare(`
|
|
4442
|
-
INSERT INTO
|
|
4470
|
+
INSERT INTO application_settings (key, value, updated_at)
|
|
4443
4471
|
VALUES (?, ?, ?)
|
|
4444
4472
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
4445
4473
|
`).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -4462,8 +4490,8 @@ var maxDiffBuffer = 10 * 1024 * 1024;
|
|
|
4462
4490
|
var githubProxyCheckArgs = ["-fsSL", "--connect-timeout", "5", "--max-time", "10", "https://www.google.com/generate_204"];
|
|
4463
4491
|
var githubProxyCountryArgs = ["-fsSL", "--connect-timeout", "5", "--max-time", "10", "https://ipinfo.io/country"];
|
|
4464
4492
|
var proxyEnvKeys = ["ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "all_proxy", "http_proxy", "https_proxy", "no_proxy"];
|
|
4465
|
-
function textFromAssistantMessage3(
|
|
4466
|
-
return
|
|
4493
|
+
function textFromAssistantMessage3(message2) {
|
|
4494
|
+
return message2.content.filter((item) => item.type === "text").map((item) => item.text).join("\n").trim();
|
|
4467
4495
|
}
|
|
4468
4496
|
async function runGit2(cwd, args) {
|
|
4469
4497
|
const { stdout } = await execFileAsync2("git", args, { cwd, maxBuffer: maxDiffBuffer });
|
|
@@ -4793,8 +4821,8 @@ async function gitHostingRoutes(app, options) {
|
|
|
4793
4821
|
}
|
|
4794
4822
|
function sendError2(reply, error, status = 500) {
|
|
4795
4823
|
const maybeStatus = error?.statusCode;
|
|
4796
|
-
const
|
|
4797
|
-
return reply.status(
|
|
4824
|
+
const errorStatus3 = typeof maybeStatus === "number" ? maybeStatus : status;
|
|
4825
|
+
return reply.status(errorStatus3).send({ error: error instanceof Error ? error.message : "Git integration request failed" });
|
|
4798
4826
|
}
|
|
4799
4827
|
|
|
4800
4828
|
// src/routes/review-sources.ts
|
|
@@ -4945,6 +4973,72 @@ async function reviewSourceRoutes(app, options) {
|
|
|
4945
4973
|
});
|
|
4946
4974
|
}
|
|
4947
4975
|
|
|
4976
|
+
// src/routes/model-window-kickoffs.ts
|
|
4977
|
+
import { stat as stat4 } from "node:fs/promises";
|
|
4978
|
+
var modelWindowKickoffRoutes = async (app, options) => {
|
|
4979
|
+
app.get("/", async () => ({ kickoffs: options.store.list() }));
|
|
4980
|
+
app.post("/", async (req, reply) => {
|
|
4981
|
+
try {
|
|
4982
|
+
const input = await validateInput(options.sessions, req.body);
|
|
4983
|
+
return reply.status(201).send({ kickoff: options.store.create(input) });
|
|
4984
|
+
} catch (error) {
|
|
4985
|
+
return reply.status(errorStatus(error, 400)).send({ error: message(error) });
|
|
4986
|
+
}
|
|
4987
|
+
});
|
|
4988
|
+
app.put("/:id", async (req, reply) => {
|
|
4989
|
+
try {
|
|
4990
|
+
const input = await validateInput(options.sessions, req.body);
|
|
4991
|
+
return { kickoff: options.store.update(req.params.id, input) };
|
|
4992
|
+
} catch (error) {
|
|
4993
|
+
const fallback = message(error).includes("not found") ? 404 : 400;
|
|
4994
|
+
return reply.status(errorStatus(error, fallback)).send({ error: message(error) });
|
|
4995
|
+
}
|
|
4996
|
+
});
|
|
4997
|
+
app.delete("/:id", async (req, reply) => {
|
|
4998
|
+
try {
|
|
4999
|
+
options.store.delete(req.params.id);
|
|
5000
|
+
return reply.status(204).send();
|
|
5001
|
+
} catch (error) {
|
|
5002
|
+
return reply.status(404).send({ error: message(error) });
|
|
5003
|
+
}
|
|
5004
|
+
});
|
|
5005
|
+
app.get("/notification-channels", async () => ({ channels: options.notifications.list() }));
|
|
5006
|
+
app.post("/notification-channels/wecom", async (req, reply) => {
|
|
5007
|
+
try {
|
|
5008
|
+
return { channel: options.notifications.saveWecom(req.body) };
|
|
5009
|
+
} catch (error) {
|
|
5010
|
+
return reply.status(400).send({ error: message(error) });
|
|
5011
|
+
}
|
|
5012
|
+
});
|
|
5013
|
+
app.delete("/notification-channels/:id", async (req, reply) => {
|
|
5014
|
+
try {
|
|
5015
|
+
options.notifications.delete(req.params.id);
|
|
5016
|
+
return reply.status(204).send();
|
|
5017
|
+
} catch (error) {
|
|
5018
|
+
return reply.status(404).send({ error: message(error) });
|
|
5019
|
+
}
|
|
5020
|
+
});
|
|
5021
|
+
};
|
|
5022
|
+
async function validateInput(sessions, input) {
|
|
5023
|
+
if (!input?.profileId || !input.provider || !input.modelId) throw new Error("Profile, provider, and model are required");
|
|
5024
|
+
const models = await sessions.listAgentProfileModels(input.profileId);
|
|
5025
|
+
if (!models.some((model) => model.provider === input.provider && model.id === input.modelId)) {
|
|
5026
|
+
throw new Error(`Unknown model: ${input.provider}/${input.modelId}`);
|
|
5027
|
+
}
|
|
5028
|
+
const projectPath = await resolveAllowedExistingPath(input.projectPath || "~");
|
|
5029
|
+
if (!(await stat4(projectPath)).isDirectory()) throw new Error("Project path must be a directory");
|
|
5030
|
+
return { ...input, projectPath };
|
|
5031
|
+
}
|
|
5032
|
+
function errorStatus(error, fallback) {
|
|
5033
|
+
return typeof error === "object" && error && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : fallback;
|
|
5034
|
+
}
|
|
5035
|
+
function message(error) {
|
|
5036
|
+
return error instanceof Error ? error.message : String(error);
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
// src/routes/auth.ts
|
|
5040
|
+
import rateLimit from "@fastify/rate-limit";
|
|
5041
|
+
|
|
4948
5042
|
// src/auth/request.ts
|
|
4949
5043
|
function getRequestContext(req, trustProxy) {
|
|
4950
5044
|
const forwardedFor = req.headers["x-forwarded-for"];
|
|
@@ -5092,17 +5186,70 @@ function mapRow2(row) {
|
|
|
5092
5186
|
};
|
|
5093
5187
|
}
|
|
5094
5188
|
|
|
5189
|
+
// src/services/user-prompt-store.ts
|
|
5190
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5191
|
+
var UserPromptStore = class {
|
|
5192
|
+
constructor(db) {
|
|
5193
|
+
this.db = db;
|
|
5194
|
+
}
|
|
5195
|
+
db;
|
|
5196
|
+
list() {
|
|
5197
|
+
return this.db.prepare(`
|
|
5198
|
+
SELECT id, name, content, created_at, updated_at
|
|
5199
|
+
FROM user_prompts
|
|
5200
|
+
ORDER BY lower(name), id
|
|
5201
|
+
`).all().map(mapRow3);
|
|
5202
|
+
}
|
|
5203
|
+
create(input) {
|
|
5204
|
+
const id = randomUUID3();
|
|
5205
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5206
|
+
this.db.prepare(`
|
|
5207
|
+
INSERT INTO user_prompts (id, name, content, created_at, updated_at)
|
|
5208
|
+
VALUES (?, ?, ?, ?, ?)
|
|
5209
|
+
`).run(id, input.name.trim(), input.content.trim(), now, now);
|
|
5210
|
+
return this.get(id);
|
|
5211
|
+
}
|
|
5212
|
+
update(id, input) {
|
|
5213
|
+
const result = this.db.prepare(`
|
|
5214
|
+
UPDATE user_prompts SET name = ?, content = ?, updated_at = ?
|
|
5215
|
+
WHERE id = ?
|
|
5216
|
+
`).run(input.name.trim(), input.content.trim(), (/* @__PURE__ */ new Date()).toISOString(), id);
|
|
5217
|
+
if (!result.changes) throw new Error(`User prompt not found: ${id}`);
|
|
5218
|
+
return this.get(id);
|
|
5219
|
+
}
|
|
5220
|
+
delete(id) {
|
|
5221
|
+
this.db.prepare("DELETE FROM user_prompts WHERE id = ?").run(id);
|
|
5222
|
+
}
|
|
5223
|
+
get(id) {
|
|
5224
|
+
const row = this.db.prepare(`
|
|
5225
|
+
SELECT id, name, content, created_at, updated_at
|
|
5226
|
+
FROM user_prompts WHERE id = ?
|
|
5227
|
+
`).get(id);
|
|
5228
|
+
return row ? mapRow3(row) : null;
|
|
5229
|
+
}
|
|
5230
|
+
};
|
|
5231
|
+
function mapRow3(row) {
|
|
5232
|
+
return {
|
|
5233
|
+
id: row.id,
|
|
5234
|
+
name: row.name,
|
|
5235
|
+
content: row.content,
|
|
5236
|
+
createdAt: row.created_at,
|
|
5237
|
+
updatedAt: row.updated_at
|
|
5238
|
+
};
|
|
5239
|
+
}
|
|
5240
|
+
|
|
5095
5241
|
// src/routes/auth.ts
|
|
5096
5242
|
async function authRoutes(app, options) {
|
|
5097
5243
|
const { config, sessions, audit, totp, rateLimiter, db } = options;
|
|
5098
5244
|
const presetStore = new SkillPresetStore(db);
|
|
5245
|
+
const userPromptStore = new UserPromptStore(db);
|
|
5099
5246
|
function getPreferenceValue(key) {
|
|
5100
|
-
const row = db.prepare("SELECT value FROM
|
|
5247
|
+
const row = db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
|
|
5101
5248
|
return row?.value;
|
|
5102
5249
|
}
|
|
5103
5250
|
function setPreferenceValue(key, value) {
|
|
5104
5251
|
db.prepare(`
|
|
5105
|
-
INSERT INTO
|
|
5252
|
+
INSERT INTO application_settings (key, value, updated_at)
|
|
5106
5253
|
VALUES (?, ?, ?)
|
|
5107
5254
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
5108
5255
|
`).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -5176,6 +5323,13 @@ async function authRoutes(app, options) {
|
|
|
5176
5323
|
}
|
|
5177
5324
|
return { name, mode, skills };
|
|
5178
5325
|
}
|
|
5326
|
+
function parseUserPromptBody(body) {
|
|
5327
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
5328
|
+
const content = typeof body.content === "string" ? body.content.trim() : "";
|
|
5329
|
+
if (!name) throw new Error("name is required");
|
|
5330
|
+
if (!content) throw new Error("content is required");
|
|
5331
|
+
return { name, content };
|
|
5332
|
+
}
|
|
5179
5333
|
function isUniqueConstraintError(error) {
|
|
5180
5334
|
return error instanceof Error && /unique/i.test(error.message);
|
|
5181
5335
|
}
|
|
@@ -5185,6 +5339,12 @@ async function authRoutes(app, options) {
|
|
|
5185
5339
|
}
|
|
5186
5340
|
return reply.status(400).send({ error: error instanceof Error ? error.message : "Invalid preset payload" });
|
|
5187
5341
|
}
|
|
5342
|
+
function sendUserPromptError(reply, error) {
|
|
5343
|
+
if (isUniqueConstraintError(error)) {
|
|
5344
|
+
return reply.status(409).send({ error: "A prompt with that name already exists" });
|
|
5345
|
+
}
|
|
5346
|
+
return reply.status(400).send({ error: error instanceof Error ? error.message : "Invalid prompt payload" });
|
|
5347
|
+
}
|
|
5188
5348
|
app.get("/me", async (req, reply) => {
|
|
5189
5349
|
const session = renewSessionFromRequest(req, reply, config, sessions);
|
|
5190
5350
|
return {
|
|
@@ -5367,6 +5527,47 @@ async function authRoutes(app, options) {
|
|
|
5367
5527
|
presetStore.delete(req.params.id, session.username);
|
|
5368
5528
|
return { success: true };
|
|
5369
5529
|
});
|
|
5530
|
+
await app.register(async (userPromptRoutes) => {
|
|
5531
|
+
await userPromptRoutes.register(rateLimit, {
|
|
5532
|
+
max: 100,
|
|
5533
|
+
timeWindow: "1 minute",
|
|
5534
|
+
keyGenerator: (req) => getRequestContext(req, config.trustProxy).ip
|
|
5535
|
+
});
|
|
5536
|
+
userPromptRoutes.get("/user-prompts", async (req, reply) => {
|
|
5537
|
+
const session = requireAuth(req, reply);
|
|
5538
|
+
if (!session) return;
|
|
5539
|
+
return { prompts: userPromptStore.list() };
|
|
5540
|
+
});
|
|
5541
|
+
userPromptRoutes.post("/user-prompts", async (req, reply) => {
|
|
5542
|
+
const session = requireAuth(req, reply);
|
|
5543
|
+
if (!session) return;
|
|
5544
|
+
try {
|
|
5545
|
+
return { prompt: userPromptStore.create(parseUserPromptBody(req.body || {})) };
|
|
5546
|
+
} catch (error) {
|
|
5547
|
+
return sendUserPromptError(reply, error);
|
|
5548
|
+
}
|
|
5549
|
+
});
|
|
5550
|
+
userPromptRoutes.patch("/user-prompts/:id", async (req, reply) => {
|
|
5551
|
+
const session = requireAuth(req, reply);
|
|
5552
|
+
if (!session) return;
|
|
5553
|
+
try {
|
|
5554
|
+
return {
|
|
5555
|
+
prompt: userPromptStore.update(
|
|
5556
|
+
req.params.id,
|
|
5557
|
+
parseUserPromptBody(req.body || {})
|
|
5558
|
+
)
|
|
5559
|
+
};
|
|
5560
|
+
} catch (error) {
|
|
5561
|
+
return sendUserPromptError(reply, error);
|
|
5562
|
+
}
|
|
5563
|
+
});
|
|
5564
|
+
userPromptRoutes.delete("/user-prompts/:id", async (req, reply) => {
|
|
5565
|
+
const session = requireAuth(req, reply);
|
|
5566
|
+
if (!session) return;
|
|
5567
|
+
userPromptStore.delete(req.params.id);
|
|
5568
|
+
return { success: true };
|
|
5569
|
+
});
|
|
5570
|
+
});
|
|
5370
5571
|
app.post("/2fa/setup", async (req, reply) => {
|
|
5371
5572
|
const session = requireAuth(req, reply);
|
|
5372
5573
|
if (!session) return;
|
|
@@ -5450,8 +5651,8 @@ function isAllowedRequestOrigin(req) {
|
|
|
5450
5651
|
var IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
5451
5652
|
var MAX_IMAGE_COUNT = 4;
|
|
5452
5653
|
var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
5453
|
-
function invalidImage(
|
|
5454
|
-
return { ok: false, code: "image_malformed", message };
|
|
5654
|
+
function invalidImage(message2 = "The image could not be read. Try selecting it again.") {
|
|
5655
|
+
return { ok: false, code: "image_malformed", message: message2 };
|
|
5455
5656
|
}
|
|
5456
5657
|
function sniffImageMimeType(bytes) {
|
|
5457
5658
|
if (bytes.subarray(0, 3).equals(Buffer.from([255, 216, 255]))) return "image/jpeg";
|
|
@@ -5542,10 +5743,10 @@ function extensionForMimeType(mimeType) {
|
|
|
5542
5743
|
}
|
|
5543
5744
|
|
|
5544
5745
|
// src/ws/safe-send.ts
|
|
5545
|
-
function sendJson(socket,
|
|
5746
|
+
function sendJson(socket, message2) {
|
|
5546
5747
|
if (socket.readyState !== socket.OPEN) return;
|
|
5547
5748
|
try {
|
|
5548
|
-
socket.send(JSON.stringify(
|
|
5749
|
+
socket.send(JSON.stringify(message2));
|
|
5549
5750
|
} catch {
|
|
5550
5751
|
}
|
|
5551
5752
|
}
|
|
@@ -5618,6 +5819,7 @@ function formatCompactResult(result, status) {
|
|
|
5618
5819
|
}
|
|
5619
5820
|
async function chatWebSocket(app) {
|
|
5620
5821
|
const sessionService = app.services.sessions;
|
|
5822
|
+
const clients = /* @__PURE__ */ new Set();
|
|
5621
5823
|
app.get("/ws/chat", { websocket: true }, (socket, req) => {
|
|
5622
5824
|
const auth = app.authServices;
|
|
5623
5825
|
if (!isAllowedRequestOrigin(req)) {
|
|
@@ -5645,7 +5847,17 @@ async function chatWebSocket(app) {
|
|
|
5645
5847
|
return;
|
|
5646
5848
|
}
|
|
5647
5849
|
const safeClientId = clientId;
|
|
5850
|
+
const connectedClient = { socket, watchedSessionId: void 0 };
|
|
5851
|
+
clients.add(connectedClient);
|
|
5648
5852
|
console.log(`Client connected: ${safeClientId}`);
|
|
5853
|
+
sessionService.cancelCleanup(safeClientId);
|
|
5854
|
+
function broadcastToSession(sessionId, message2) {
|
|
5855
|
+
for (const client of clients) {
|
|
5856
|
+
if (client === connectedClient || client.watchedSessionId === sessionId) {
|
|
5857
|
+
sendJson(client.socket, message2);
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5649
5861
|
function sendPromptPreflight(sessionId, requestId, accepted, error, imagePaths) {
|
|
5650
5862
|
sendJson(socket, {
|
|
5651
5863
|
type: "prompt_preflight",
|
|
@@ -5709,10 +5921,24 @@ async function chatWebSocket(app) {
|
|
|
5709
5921
|
}
|
|
5710
5922
|
socket.on("message", async (rawData) => {
|
|
5711
5923
|
try {
|
|
5712
|
-
const
|
|
5713
|
-
switch (
|
|
5924
|
+
const message2 = JSON.parse(rawData.toString());
|
|
5925
|
+
switch (message2.type) {
|
|
5926
|
+
case "watch": {
|
|
5927
|
+
connectedClient.watchedSessionId = message2.payload?.sessionId;
|
|
5928
|
+
const targetSessionId = connectedClient.watchedSessionId;
|
|
5929
|
+
if (targetSessionId) {
|
|
5930
|
+
const isStreaming = sessionService.isSessionStreaming(targetSessionId);
|
|
5931
|
+
sendJson(socket, {
|
|
5932
|
+
type: "streaming_state",
|
|
5933
|
+
sessionId: targetSessionId,
|
|
5934
|
+
isStreaming,
|
|
5935
|
+
...isStreaming ? { streamingStartedAt: sessionService.getSessionStreamingStartedAt(targetSessionId) } : {}
|
|
5936
|
+
});
|
|
5937
|
+
}
|
|
5938
|
+
break;
|
|
5939
|
+
}
|
|
5714
5940
|
case "prompt": {
|
|
5715
|
-
const session2 = await ensureTargetSession(
|
|
5941
|
+
const session2 = await ensureTargetSession(message2.payload?.sessionId);
|
|
5716
5942
|
if (!session2) {
|
|
5717
5943
|
sendJson(socket, {
|
|
5718
5944
|
type: "error",
|
|
@@ -5720,14 +5946,16 @@ async function chatWebSocket(app) {
|
|
|
5720
5946
|
});
|
|
5721
5947
|
return;
|
|
5722
5948
|
}
|
|
5723
|
-
const imageResult = validateImages(
|
|
5949
|
+
const imageResult = validateImages(message2.payload?.images, session2.model);
|
|
5724
5950
|
if (!imageResult.ok) {
|
|
5725
|
-
sendPromptPreflight(session2.sessionId,
|
|
5951
|
+
sendPromptPreflight(session2.sessionId, message2.payload?.requestId, false, imageResult);
|
|
5726
5952
|
return;
|
|
5727
5953
|
}
|
|
5728
5954
|
sessionService.cancelCleanup(safeClientId);
|
|
5729
5955
|
const sessionId = session2.sessionId;
|
|
5730
|
-
|
|
5956
|
+
connectedClient.watchedSessionId = sessionId;
|
|
5957
|
+
const streamingStartedAt = sessionService.markSessionStreamingStarted(sessionId);
|
|
5958
|
+
const compactInstructions = parseCompactCommand(message2.payload?.text || "");
|
|
5731
5959
|
req.log?.info({
|
|
5732
5960
|
clientId: safeClientId,
|
|
5733
5961
|
sessionId,
|
|
@@ -5751,7 +5979,7 @@ async function chatWebSocket(app) {
|
|
|
5751
5979
|
if (recent.every((t) => t.name === recent[0].name && t.input === recent[0].input)) {
|
|
5752
5980
|
loopDetected = true;
|
|
5753
5981
|
console.warn(`[loop-detected] tool="${name}" sessionId=${sessionId}`);
|
|
5754
|
-
|
|
5982
|
+
broadcastToSession(sessionId, {
|
|
5755
5983
|
type: "loop_detected",
|
|
5756
5984
|
sessionId,
|
|
5757
5985
|
message: `Loop detected: tool "${name}" called ${MAX_REPEATED_TOOL_CALLS} times with identical input. Stopping automatically.`,
|
|
@@ -5766,10 +5994,11 @@ async function chatWebSocket(app) {
|
|
|
5766
5994
|
}
|
|
5767
5995
|
}
|
|
5768
5996
|
}
|
|
5769
|
-
|
|
5997
|
+
broadcastToSession(sessionId, {
|
|
5770
5998
|
type: "event",
|
|
5771
5999
|
sessionId,
|
|
5772
|
-
event
|
|
6000
|
+
event,
|
|
6001
|
+
streamingStartedAt
|
|
5773
6002
|
});
|
|
5774
6003
|
});
|
|
5775
6004
|
try {
|
|
@@ -5783,9 +6012,9 @@ async function chatWebSocket(app) {
|
|
|
5783
6012
|
result
|
|
5784
6013
|
});
|
|
5785
6014
|
} else {
|
|
5786
|
-
const requestId =
|
|
6015
|
+
const requestId = message2.payload?.requestId;
|
|
5787
6016
|
const preparedPrompt = await promptWithUploadedImagePaths(
|
|
5788
|
-
|
|
6017
|
+
message2.payload?.text || "",
|
|
5789
6018
|
session2,
|
|
5790
6019
|
imageResult
|
|
5791
6020
|
);
|
|
@@ -5803,24 +6032,23 @@ async function chatWebSocket(app) {
|
|
|
5803
6032
|
});
|
|
5804
6033
|
} finally {
|
|
5805
6034
|
unsubscribe();
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
}
|
|
6035
|
+
sessionService.markSessionStreamingFinished(sessionId);
|
|
6036
|
+
broadcastToSession(sessionId, { type: "status", sessionId, status: "idle" });
|
|
5809
6037
|
}
|
|
5810
6038
|
break;
|
|
5811
6039
|
}
|
|
5812
6040
|
case "steer":
|
|
5813
6041
|
case "followUp": {
|
|
5814
|
-
const session2 = await ensureTargetSession(
|
|
6042
|
+
const session2 = await ensureTargetSession(message2.payload?.sessionId);
|
|
5815
6043
|
if (!session2) break;
|
|
5816
|
-
const imageResult = validateImages(
|
|
6044
|
+
const imageResult = validateImages(message2.payload?.images, session2.model);
|
|
5817
6045
|
if (!imageResult.ok) {
|
|
5818
|
-
sendPromptPreflight(session2.sessionId,
|
|
6046
|
+
sendPromptPreflight(session2.sessionId, message2.payload?.requestId, false, imageResult);
|
|
5819
6047
|
break;
|
|
5820
6048
|
}
|
|
5821
|
-
if (!
|
|
5822
|
-
const requestId =
|
|
5823
|
-
const streamingBehavior =
|
|
6049
|
+
if (!message2.payload?.text && imageResult.images.length === 0) break;
|
|
6050
|
+
const requestId = message2.payload?.requestId;
|
|
6051
|
+
const streamingBehavior = message2.type === "steer" ? "steer" : "followUp";
|
|
5824
6052
|
req.log?.info({
|
|
5825
6053
|
clientId: safeClientId,
|
|
5826
6054
|
sessionId: session2.sessionId,
|
|
@@ -5830,7 +6058,7 @@ async function chatWebSocket(app) {
|
|
|
5830
6058
|
}, "[ai-chat] request");
|
|
5831
6059
|
await sessionService.runForegroundWithClientProfileProxy(safeClientId, async () => {
|
|
5832
6060
|
const preparedPrompt = await promptWithUploadedImagePaths(
|
|
5833
|
-
|
|
6061
|
+
message2.payload?.text || "",
|
|
5834
6062
|
session2,
|
|
5835
6063
|
imageResult
|
|
5836
6064
|
);
|
|
@@ -5843,7 +6071,7 @@ async function chatWebSocket(app) {
|
|
|
5843
6071
|
break;
|
|
5844
6072
|
}
|
|
5845
6073
|
case "abort": {
|
|
5846
|
-
const session2 = await ensureTargetSession(
|
|
6074
|
+
const session2 = await ensureTargetSession(message2.payload?.sessionId);
|
|
5847
6075
|
if (session2) {
|
|
5848
6076
|
const aborted = await abortSessionBestEffort(session2);
|
|
5849
6077
|
if (!aborted) {
|
|
@@ -5854,7 +6082,8 @@ async function chatWebSocket(app) {
|
|
|
5854
6082
|
message: "Request did not stop cleanly; the stuck runtime was discarded."
|
|
5855
6083
|
});
|
|
5856
6084
|
}
|
|
5857
|
-
|
|
6085
|
+
sessionService.markSessionStreamingFinished(session2.sessionId);
|
|
6086
|
+
broadcastToSession(session2.sessionId, { type: "status", sessionId: session2.sessionId, status: "idle" });
|
|
5858
6087
|
}
|
|
5859
6088
|
break;
|
|
5860
6089
|
}
|
|
@@ -5868,17 +6097,10 @@ async function chatWebSocket(app) {
|
|
|
5868
6097
|
}
|
|
5869
6098
|
});
|
|
5870
6099
|
socket.on("close", () => {
|
|
6100
|
+
clients.delete(connectedClient);
|
|
5871
6101
|
console.log(`Client disconnected: ${safeClientId}`);
|
|
5872
6102
|
unsubscribeMemoryUpdates();
|
|
5873
6103
|
unsubscribeMemoryRecall();
|
|
5874
|
-
const activeSession = sessionService.getSession(safeClientId);
|
|
5875
|
-
if (activeSession?.isStreaming) {
|
|
5876
|
-
abortSessionBestEffort(activeSession).then((aborted) => {
|
|
5877
|
-
if (!aborted) sessionService.forceDisposeBySessionId(activeSession.sessionId);
|
|
5878
|
-
}).catch((error) => {
|
|
5879
|
-
console.warn("Failed to stop streaming session on disconnect:", error);
|
|
5880
|
-
});
|
|
5881
|
-
}
|
|
5882
6104
|
sessionService.scheduleCleanup(safeClientId);
|
|
5883
6105
|
});
|
|
5884
6106
|
});
|
|
@@ -5946,16 +6168,16 @@ async function terminalWebSocket(app) {
|
|
|
5946
6168
|
}));
|
|
5947
6169
|
socket.on("message", (rawData) => {
|
|
5948
6170
|
try {
|
|
5949
|
-
const
|
|
5950
|
-
switch (
|
|
6171
|
+
const message2 = JSON.parse(rawData.toString());
|
|
6172
|
+
switch (message2.type) {
|
|
5951
6173
|
case "input":
|
|
5952
|
-
if (
|
|
5953
|
-
terminalManager.writeTo(terminal.id,
|
|
6174
|
+
if (message2.terminalId === terminal.id && typeof message2.data === "string") {
|
|
6175
|
+
terminalManager.writeTo(terminal.id, message2.data);
|
|
5954
6176
|
}
|
|
5955
6177
|
break;
|
|
5956
6178
|
case "resize":
|
|
5957
|
-
if (
|
|
5958
|
-
terminalManager.resize(terminal.id,
|
|
6179
|
+
if (message2.terminalId === terminal.id && Number.isFinite(message2.cols) && Number.isFinite(message2.rows)) {
|
|
6180
|
+
terminalManager.resize(terminal.id, message2.cols, message2.rows);
|
|
5959
6181
|
}
|
|
5960
6182
|
break;
|
|
5961
6183
|
}
|
|
@@ -6534,12 +6756,117 @@ var wecomGatewaySchemaMigration = {
|
|
|
6534
6756
|
}
|
|
6535
6757
|
};
|
|
6536
6758
|
|
|
6759
|
+
// src/db/migrations/005-model-window-kickoff-schema.ts
|
|
6760
|
+
var modelWindowKickoffSchemaMigration = {
|
|
6761
|
+
version: 5,
|
|
6762
|
+
name: "model-window-kickoff-schema",
|
|
6763
|
+
up(db) {
|
|
6764
|
+
db.exec(`
|
|
6765
|
+
CREATE TABLE IF NOT EXISTS notification_channels (
|
|
6766
|
+
id TEXT PRIMARY KEY,
|
|
6767
|
+
type TEXT NOT NULL,
|
|
6768
|
+
name TEXT NOT NULL,
|
|
6769
|
+
config_json TEXT NOT NULL,
|
|
6770
|
+
created_at TEXT NOT NULL,
|
|
6771
|
+
updated_at TEXT NOT NULL
|
|
6772
|
+
);
|
|
6773
|
+
|
|
6774
|
+
CREATE TABLE IF NOT EXISTS model_window_kickoffs (
|
|
6775
|
+
id TEXT PRIMARY KEY,
|
|
6776
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
6777
|
+
profile_id TEXT NOT NULL,
|
|
6778
|
+
provider TEXT NOT NULL,
|
|
6779
|
+
model_id TEXT NOT NULL,
|
|
6780
|
+
prompt TEXT NOT NULL,
|
|
6781
|
+
window_duration_minutes INTEGER NOT NULL,
|
|
6782
|
+
safety_buffer_seconds INTEGER NOT NULL,
|
|
6783
|
+
notification_channel_id TEXT REFERENCES notification_channels(id) ON DELETE SET NULL,
|
|
6784
|
+
next_run_at TEXT NOT NULL,
|
|
6785
|
+
last_attempt_at TEXT,
|
|
6786
|
+
last_success_at TEXT,
|
|
6787
|
+
last_error TEXT,
|
|
6788
|
+
created_at TEXT NOT NULL,
|
|
6789
|
+
updated_at TEXT NOT NULL
|
|
6790
|
+
);
|
|
6791
|
+
|
|
6792
|
+
CREATE INDEX IF NOT EXISTS model_window_kickoffs_due_idx
|
|
6793
|
+
ON model_window_kickoffs(enabled, next_run_at);
|
|
6794
|
+
`);
|
|
6795
|
+
}
|
|
6796
|
+
};
|
|
6797
|
+
|
|
6798
|
+
// src/db/migrations/006-model-window-kickoff-project-path.ts
|
|
6799
|
+
var modelWindowKickoffProjectPathMigration = {
|
|
6800
|
+
version: 6,
|
|
6801
|
+
name: "model-window-kickoff-project-path",
|
|
6802
|
+
up(db) {
|
|
6803
|
+
db.exec(`
|
|
6804
|
+
ALTER TABLE model_window_kickoffs
|
|
6805
|
+
ADD COLUMN project_path TEXT NOT NULL DEFAULT '~';
|
|
6806
|
+
`);
|
|
6807
|
+
}
|
|
6808
|
+
};
|
|
6809
|
+
|
|
6810
|
+
// src/db/migrations/007-user-prompts-schema.ts
|
|
6811
|
+
var userPromptsSchemaMigration = {
|
|
6812
|
+
version: 7,
|
|
6813
|
+
name: "user-prompts-schema",
|
|
6814
|
+
up(db) {
|
|
6815
|
+
db.exec(`
|
|
6816
|
+
CREATE TABLE user_prompts (
|
|
6817
|
+
id TEXT PRIMARY KEY,
|
|
6818
|
+
name TEXT NOT NULL UNIQUE,
|
|
6819
|
+
content TEXT NOT NULL,
|
|
6820
|
+
created_at TEXT NOT NULL,
|
|
6821
|
+
updated_at TEXT NOT NULL
|
|
6822
|
+
)
|
|
6823
|
+
`);
|
|
6824
|
+
}
|
|
6825
|
+
};
|
|
6826
|
+
|
|
6827
|
+
// src/db/migrations/008-application-settings-table.ts
|
|
6828
|
+
var applicationSettingsTableMigration = {
|
|
6829
|
+
version: 8,
|
|
6830
|
+
name: "application-settings-table",
|
|
6831
|
+
up(db) {
|
|
6832
|
+
db.exec("ALTER TABLE security_settings RENAME TO application_settings");
|
|
6833
|
+
}
|
|
6834
|
+
};
|
|
6835
|
+
|
|
6836
|
+
// src/db/migrations/009-pinned-files-schema.ts
|
|
6837
|
+
var pinnedFilesSchemaMigration = {
|
|
6838
|
+
version: 9,
|
|
6839
|
+
name: "pinned-files-schema",
|
|
6840
|
+
up(db) {
|
|
6841
|
+
db.exec(`
|
|
6842
|
+
CREATE TABLE pinned_files (
|
|
6843
|
+
owner_type TEXT NOT NULL CHECK (owner_type = 'profile'),
|
|
6844
|
+
owner_id TEXT NOT NULL,
|
|
6845
|
+
file_path TEXT NOT NULL,
|
|
6846
|
+
group_id TEXT NOT NULL,
|
|
6847
|
+
created_at TEXT NOT NULL,
|
|
6848
|
+
PRIMARY KEY (owner_type, owner_id, file_path),
|
|
6849
|
+
FOREIGN KEY (owner_type, owner_id, group_id)
|
|
6850
|
+
REFERENCES session_pin_groups(owner_type, owner_id, id) ON DELETE CASCADE
|
|
6851
|
+
);
|
|
6852
|
+
|
|
6853
|
+
CREATE INDEX pinned_files_group_idx
|
|
6854
|
+
ON pinned_files(owner_type, owner_id, group_id, created_at);
|
|
6855
|
+
`);
|
|
6856
|
+
}
|
|
6857
|
+
};
|
|
6858
|
+
|
|
6537
6859
|
// src/db/migrations/index.ts
|
|
6538
6860
|
var migrations = [
|
|
6539
6861
|
applicationSchemaMigration,
|
|
6540
6862
|
gatewaySchemaMigration,
|
|
6541
6863
|
projectHistorySchemaMigration,
|
|
6542
|
-
wecomGatewaySchemaMigration
|
|
6864
|
+
wecomGatewaySchemaMigration,
|
|
6865
|
+
modelWindowKickoffSchemaMigration,
|
|
6866
|
+
modelWindowKickoffProjectPathMigration,
|
|
6867
|
+
userPromptsSchemaMigration,
|
|
6868
|
+
applicationSettingsTableMigration,
|
|
6869
|
+
pinnedFilesSchemaMigration
|
|
6543
6870
|
];
|
|
6544
6871
|
function runDatabaseMigrations(db) {
|
|
6545
6872
|
db.exec(`
|
|
@@ -6787,18 +7114,18 @@ var TotpService = class {
|
|
|
6787
7114
|
this.deleteValue("totp.secret");
|
|
6788
7115
|
}
|
|
6789
7116
|
getValue(key) {
|
|
6790
|
-
const row = this.db.prepare("SELECT value FROM
|
|
7117
|
+
const row = this.db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
|
|
6791
7118
|
return row?.value;
|
|
6792
7119
|
}
|
|
6793
7120
|
setValue(key, value) {
|
|
6794
7121
|
this.db.prepare(`
|
|
6795
|
-
INSERT INTO
|
|
7122
|
+
INSERT INTO application_settings (key, value, updated_at)
|
|
6796
7123
|
VALUES (?, ?, ?)
|
|
6797
7124
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
6798
7125
|
`).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
|
|
6799
7126
|
}
|
|
6800
7127
|
deleteValue(key) {
|
|
6801
|
-
this.db.prepare("DELETE FROM
|
|
7128
|
+
this.db.prepare("DELETE FROM application_settings WHERE key = ?").run(key);
|
|
6802
7129
|
}
|
|
6803
7130
|
};
|
|
6804
7131
|
|
|
@@ -7126,6 +7453,7 @@ var PiSessionService = class {
|
|
|
7126
7453
|
clientSessions = /* @__PURE__ */ new Map();
|
|
7127
7454
|
currentClientSession = /* @__PURE__ */ new Map();
|
|
7128
7455
|
cleanupTimers = /* @__PURE__ */ new Map();
|
|
7456
|
+
streamingStartedAt = /* @__PURE__ */ new Map();
|
|
7129
7457
|
clientProfiles = /* @__PURE__ */ new Map();
|
|
7130
7458
|
sessionListCache = /* @__PURE__ */ new Map();
|
|
7131
7459
|
userMessageCountCache = /* @__PURE__ */ new Map();
|
|
@@ -7873,7 +8201,7 @@ var PiSessionService = class {
|
|
|
7873
8201
|
path: sessionPath || cwd,
|
|
7874
8202
|
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7875
8203
|
modified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7876
|
-
messageCount: activeSession.messages.filter((
|
|
8204
|
+
messageCount: activeSession.messages.filter((message2) => message2.role === "user").length,
|
|
7877
8205
|
cwd,
|
|
7878
8206
|
firstMessage: typeof firstUserMessage === "string" ? firstUserMessage : void 0,
|
|
7879
8207
|
isStreaming: activeSession.isStreaming
|
|
@@ -8292,12 +8620,32 @@ var PiSessionService = class {
|
|
|
8292
8620
|
isSessionStreaming(sessionId) {
|
|
8293
8621
|
return this.getSessionBySessionId(sessionId)?.isStreaming === true;
|
|
8294
8622
|
}
|
|
8623
|
+
markSessionStreamingStarted(sessionId) {
|
|
8624
|
+
const startedAt = this.streamingStartedAt.get(sessionId) ?? Date.now();
|
|
8625
|
+
this.streamingStartedAt.set(sessionId, startedAt);
|
|
8626
|
+
return startedAt;
|
|
8627
|
+
}
|
|
8628
|
+
getSessionStreamingStartedAt(sessionId) {
|
|
8629
|
+
const startedAt = this.streamingStartedAt.get(sessionId);
|
|
8630
|
+
if (startedAt !== void 0 || !this.isSessionStreaming(sessionId)) return startedAt;
|
|
8631
|
+
return this.markSessionStreamingStarted(sessionId);
|
|
8632
|
+
}
|
|
8633
|
+
markSessionStreamingFinished(sessionId) {
|
|
8634
|
+
this.streamingStartedAt.delete(sessionId);
|
|
8635
|
+
}
|
|
8295
8636
|
isCwdStreaming(cwd) {
|
|
8296
8637
|
return Array.from(this.sessions.values()).some((session) => session.sessionManager.getCwd() === cwd && session.isStreaming === true);
|
|
8297
8638
|
}
|
|
8298
8639
|
scheduleCleanup(clientId, delayMs = 5 * 60 * 1e3) {
|
|
8299
8640
|
this.cancelCleanup(clientId);
|
|
8300
8641
|
const timer = setTimeout(() => {
|
|
8642
|
+
this.cleanupTimers.delete(clientId);
|
|
8643
|
+
const sessionIds = this.clientSessions.get(clientId) || /* @__PURE__ */ new Set();
|
|
8644
|
+
const isStreaming = Array.from(sessionIds).some((sessionId) => this.sessions.get(sessionId)?.isStreaming);
|
|
8645
|
+
if (isStreaming) {
|
|
8646
|
+
this.scheduleCleanup(clientId, delayMs);
|
|
8647
|
+
return;
|
|
8648
|
+
}
|
|
8301
8649
|
this.disposeSession(clientId);
|
|
8302
8650
|
}, delayMs);
|
|
8303
8651
|
this.cleanupTimers.set(clientId, timer);
|
|
@@ -8317,6 +8665,7 @@ var PiSessionService = class {
|
|
|
8317
8665
|
if (!session) continue;
|
|
8318
8666
|
session.dispose();
|
|
8319
8667
|
this.sessions.delete(sessionId);
|
|
8668
|
+
this.streamingStartedAt.delete(sessionId);
|
|
8320
8669
|
}
|
|
8321
8670
|
this.clientSessions.delete(clientId);
|
|
8322
8671
|
this.currentClientSession.delete(clientId);
|
|
@@ -8342,6 +8691,7 @@ var PiSessionService = class {
|
|
|
8342
8691
|
session.dispose();
|
|
8343
8692
|
this.sessions.delete(sessionId);
|
|
8344
8693
|
}
|
|
8694
|
+
this.streamingStartedAt.delete(sessionId);
|
|
8345
8695
|
for (const [clientId, sessionIds] of this.clientSessions) {
|
|
8346
8696
|
sessionIds.delete(sessionId);
|
|
8347
8697
|
if (this.currentClientSession.get(clientId) === sessionId) {
|
|
@@ -8562,7 +8912,7 @@ var PiSessionService = class {
|
|
|
8562
8912
|
return (await this.readSessionDirHeader(sessionDir))?.cwd;
|
|
8563
8913
|
}
|
|
8564
8914
|
getFirstUserMessageContent(messages) {
|
|
8565
|
-
const firstUserMessage = messages.find((
|
|
8915
|
+
const firstUserMessage = messages.find((message2) => Boolean(message2) && typeof message2 === "object" && message2.role === "user");
|
|
8566
8916
|
return typeof firstUserMessage?.content === "string" ? firstUserMessage.content : void 0;
|
|
8567
8917
|
}
|
|
8568
8918
|
async readSessionDirHeader(sessionDir) {
|
|
@@ -8678,7 +9028,7 @@ var SessionActivityStore = class {
|
|
|
8678
9028
|
WHERE session_id = ?
|
|
8679
9029
|
ORDER BY created_at ASC, id ASC
|
|
8680
9030
|
`).all(sessionId);
|
|
8681
|
-
return rows.map(
|
|
9031
|
+
return rows.map(mapRow4);
|
|
8682
9032
|
}
|
|
8683
9033
|
updatePrStatus(id, status, checkedAt = this.now()) {
|
|
8684
9034
|
const row = this.db.prepare("SELECT session_id, data_json FROM session_builtin_events WHERE id = ? AND kind = 'pr_created'").get(id);
|
|
@@ -8700,7 +9050,7 @@ var SessionActivityStore = class {
|
|
|
8700
9050
|
ORDER BY created_at DESC, id DESC
|
|
8701
9051
|
`).all(...sessionIds);
|
|
8702
9052
|
for (const row of rows) {
|
|
8703
|
-
if (!latest.has(row.session_id)) latest.set(row.session_id,
|
|
9053
|
+
if (!latest.has(row.session_id)) latest.set(row.session_id, mapRow4(row));
|
|
8704
9054
|
}
|
|
8705
9055
|
return latest;
|
|
8706
9056
|
}
|
|
@@ -8713,7 +9063,7 @@ var SessionActivityStore = class {
|
|
|
8713
9063
|
return this.listForSession(sessionId).find((record) => record.id === Number(result.lastInsertRowid));
|
|
8714
9064
|
}
|
|
8715
9065
|
};
|
|
8716
|
-
function
|
|
9066
|
+
function mapRow4(row) {
|
|
8717
9067
|
return {
|
|
8718
9068
|
id: row.id,
|
|
8719
9069
|
sessionId: row.session_id,
|
|
@@ -8732,7 +9082,7 @@ function parseJson(value) {
|
|
|
8732
9082
|
}
|
|
8733
9083
|
|
|
8734
9084
|
// src/services/session-pin-store.ts
|
|
8735
|
-
import { randomUUID as
|
|
9085
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
8736
9086
|
var DEFAULT_PIN_GROUP_ID = "default";
|
|
8737
9087
|
var SessionPinStore = class {
|
|
8738
9088
|
constructor(db) {
|
|
@@ -8753,7 +9103,7 @@ var SessionPinStore = class {
|
|
|
8753
9103
|
this.ensureDefaultGroup(owner);
|
|
8754
9104
|
const trimmed = name.trim();
|
|
8755
9105
|
if (!trimmed) throw new Error("Group name is required");
|
|
8756
|
-
const id =
|
|
9106
|
+
const id = randomUUID4();
|
|
8757
9107
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8758
9108
|
this.db.prepare(`
|
|
8759
9109
|
INSERT INTO session_pin_groups (owner_type, owner_id, id, name, is_default, created_at)
|
|
@@ -8779,6 +9129,40 @@ var SessionPinStore = class {
|
|
|
8779
9129
|
DELETE FROM session_pins WHERE owner_type = ? AND owner_id = ? AND session_id = ?
|
|
8780
9130
|
`).run(owner.type, owner.id, sessionId);
|
|
8781
9131
|
}
|
|
9132
|
+
pinFile(profileId, filePath, groupId) {
|
|
9133
|
+
const owner = { type: "profile", id: profileId };
|
|
9134
|
+
this.ensureDefaultGroup(owner);
|
|
9135
|
+
if (!this.db.prepare(`
|
|
9136
|
+
SELECT 1 FROM session_pin_groups WHERE owner_type = 'profile' AND owner_id = ? AND id = ?
|
|
9137
|
+
`).get(profileId, groupId)) {
|
|
9138
|
+
throw new Error("Pin group not found");
|
|
9139
|
+
}
|
|
9140
|
+
this.db.prepare(`
|
|
9141
|
+
INSERT INTO pinned_files (owner_type, owner_id, file_path, group_id, created_at)
|
|
9142
|
+
VALUES ('profile', ?, ?, ?, ?)
|
|
9143
|
+
ON CONFLICT(owner_type, owner_id, file_path) DO UPDATE SET group_id = excluded.group_id
|
|
9144
|
+
`).run(profileId, filePath, groupId, (/* @__PURE__ */ new Date()).toISOString());
|
|
9145
|
+
}
|
|
9146
|
+
unpinFile(profileId, filePath) {
|
|
9147
|
+
this.db.prepare(`
|
|
9148
|
+
DELETE FROM pinned_files WHERE owner_type = 'profile' AND owner_id = ? AND file_path = ?
|
|
9149
|
+
`).run(profileId, filePath);
|
|
9150
|
+
}
|
|
9151
|
+
listFilePathsByGroup(profileId) {
|
|
9152
|
+
const rows = this.db.prepare(`
|
|
9153
|
+
SELECT group_id, file_path
|
|
9154
|
+
FROM pinned_files
|
|
9155
|
+
WHERE owner_type = 'profile' AND owner_id = ?
|
|
9156
|
+
ORDER BY created_at, file_path
|
|
9157
|
+
`).all(profileId);
|
|
9158
|
+
const result = /* @__PURE__ */ new Map();
|
|
9159
|
+
for (const row of rows) {
|
|
9160
|
+
const paths = result.get(row.group_id) || [];
|
|
9161
|
+
paths.push(row.file_path);
|
|
9162
|
+
result.set(row.group_id, paths);
|
|
9163
|
+
}
|
|
9164
|
+
return result;
|
|
9165
|
+
}
|
|
8782
9166
|
listSessionIdsByGroup(owner) {
|
|
8783
9167
|
const rows = this.db.prepare(`
|
|
8784
9168
|
SELECT group_id, session_id
|
|
@@ -8833,7 +9217,7 @@ var SkillPolicyStore = class {
|
|
|
8833
9217
|
}
|
|
8834
9218
|
get(sessionId) {
|
|
8835
9219
|
const row = this.db.prepare("SELECT * FROM session_skill_policies WHERE session_id = ?").get(sessionId);
|
|
8836
|
-
return row ?
|
|
9220
|
+
return row ? mapRow5(row) : null;
|
|
8837
9221
|
}
|
|
8838
9222
|
delete(sessionId) {
|
|
8839
9223
|
this.db.prepare("DELETE FROM session_skill_policies WHERE session_id = ?").run(sessionId);
|
|
@@ -8842,7 +9226,7 @@ var SkillPolicyStore = class {
|
|
|
8842
9226
|
function normalizeSkills2(skills) {
|
|
8843
9227
|
return Array.from(new Set(skills.map((skill) => skill.trim()).filter(Boolean)));
|
|
8844
9228
|
}
|
|
8845
|
-
function
|
|
9229
|
+
function mapRow5(row) {
|
|
8846
9230
|
return {
|
|
8847
9231
|
sessionId: row.session_id,
|
|
8848
9232
|
username: row.username,
|
|
@@ -9025,16 +9409,16 @@ function buildExtractionSource(branchEntries, startingLeafId, endingLeafId) {
|
|
|
9025
9409
|
const evidence = [];
|
|
9026
9410
|
for (const entry of branchEntries.slice(startIndex + 1, endIndex + 1)) {
|
|
9027
9411
|
if (entry.type !== "message") continue;
|
|
9028
|
-
const
|
|
9029
|
-
if (
|
|
9030
|
-
for (const text of textBlocks(
|
|
9412
|
+
const message2 = entry.message;
|
|
9413
|
+
if (message2.role === "user") {
|
|
9414
|
+
for (const text of textBlocks(message2.content)) {
|
|
9031
9415
|
lines.push(`[User] ${text}`);
|
|
9032
9416
|
evidence.push({ role: "user", text });
|
|
9033
9417
|
}
|
|
9034
9418
|
continue;
|
|
9035
9419
|
}
|
|
9036
|
-
if (
|
|
9037
|
-
for (const block of
|
|
9420
|
+
if (message2.role !== "assistant" || !Array.isArray(message2.content)) continue;
|
|
9421
|
+
for (const block of message2.content) {
|
|
9038
9422
|
if (block.type === "text" && block.text.trim()) {
|
|
9039
9423
|
const text = block.text.trim();
|
|
9040
9424
|
lines.push(`[Assistant] ${text}`);
|
|
@@ -9474,15 +9858,15 @@ function buildExtractionSource2(branchEntries, startingLeafId, endingLeafId) {
|
|
|
9474
9858
|
};
|
|
9475
9859
|
for (const entry of branchEntries.slice(startIndex + 1, endIndex + 1)) {
|
|
9476
9860
|
if (entry.type !== "message") continue;
|
|
9477
|
-
const
|
|
9478
|
-
if (
|
|
9479
|
-
for (const text2 of textBlocks2(
|
|
9861
|
+
const message2 = entry.message;
|
|
9862
|
+
if (message2.role === "user") {
|
|
9863
|
+
for (const text2 of textBlocks2(message2.content)) {
|
|
9480
9864
|
if (!isRawNoise(text2)) append({ role: "user", text: text2, ...textMetadata(text2) });
|
|
9481
9865
|
}
|
|
9482
9866
|
continue;
|
|
9483
9867
|
}
|
|
9484
|
-
if (
|
|
9485
|
-
for (const block of
|
|
9868
|
+
if (message2.role !== "assistant" || !Array.isArray(message2.content)) continue;
|
|
9869
|
+
for (const block of message2.content) {
|
|
9486
9870
|
if (block.type === "text" && isConfirmedAssistantEvidence(block.text)) {
|
|
9487
9871
|
append({ role: "assistant", text: block.text, ...textMetadata(block.text) });
|
|
9488
9872
|
continue;
|
|
@@ -9984,8 +10368,8 @@ function buildExtractionQueries(source) {
|
|
|
9984
10368
|
// src/memory/extractor.ts
|
|
9985
10369
|
var EXTRACTION_TIMEOUT_MS = 45e3;
|
|
9986
10370
|
var RetryableExtractionError = class extends Error {
|
|
9987
|
-
constructor(kind,
|
|
9988
|
-
super(
|
|
10371
|
+
constructor(kind, message2) {
|
|
10372
|
+
super(message2);
|
|
9989
10373
|
this.kind = kind;
|
|
9990
10374
|
this.name = "RetryableExtractionError";
|
|
9991
10375
|
}
|
|
@@ -10349,21 +10733,21 @@ function isUsage(value) {
|
|
|
10349
10733
|
function isAbort(error, signal) {
|
|
10350
10734
|
return signal.aborted || error instanceof Error && error.name === "AbortError";
|
|
10351
10735
|
}
|
|
10352
|
-
function providerResponseError(
|
|
10353
|
-
return new Error(
|
|
10736
|
+
function providerResponseError(message2) {
|
|
10737
|
+
return new Error(message2?.trim() || "Memory extraction provider failed");
|
|
10354
10738
|
}
|
|
10355
10739
|
function isUnsupportedSchemaError(error) {
|
|
10356
|
-
const status =
|
|
10357
|
-
const
|
|
10358
|
-
return (status === 400 || status === 422 || status === void 0) && /(?:response[_ .-]?format|output[_ .-]?config|text\.format|json[_ .-]?schema|structured output|responseJsonSchema|unknown (?:field|parameter)).*(?:unsupported|unknown|invalid|not (?:allowed|supported))|(?:unsupported|unknown|invalid|not (?:allowed|supported)).*(?:response[_ .-]?format|output[_ .-]?config|text\.format|json[_ .-]?schema|structured output|responseJsonSchema)/i.test(
|
|
10740
|
+
const status = errorStatus2(error);
|
|
10741
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
10742
|
+
return (status === 400 || status === 422 || status === void 0) && /(?:response[_ .-]?format|output[_ .-]?config|text\.format|json[_ .-]?schema|structured output|responseJsonSchema|unknown (?:field|parameter)).*(?:unsupported|unknown|invalid|not (?:allowed|supported))|(?:unsupported|unknown|invalid|not (?:allowed|supported)).*(?:response[_ .-]?format|output[_ .-]?config|text\.format|json[_ .-]?schema|structured output|responseJsonSchema)/i.test(message2);
|
|
10359
10743
|
}
|
|
10360
10744
|
function isRetryableTransportError(error) {
|
|
10361
|
-
const status =
|
|
10745
|
+
const status = errorStatus2(error);
|
|
10362
10746
|
if (status !== void 0) return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
|
|
10363
10747
|
if (!(error instanceof Error)) return false;
|
|
10364
10748
|
return /(?:timeout|timed out|network|connection|socket|fetch failed|temporarily unavailable|rate limit|overloaded)/i.test(error.message);
|
|
10365
10749
|
}
|
|
10366
|
-
function
|
|
10750
|
+
function errorStatus2(error) {
|
|
10367
10751
|
if (!error || typeof error !== "object") return void 0;
|
|
10368
10752
|
const candidate = error;
|
|
10369
10753
|
const status = candidate.status ?? candidate.statusCode ?? candidate.response?.status;
|
|
@@ -10997,15 +11381,15 @@ function errorName2(error) {
|
|
|
10997
11381
|
}
|
|
10998
11382
|
|
|
10999
11383
|
// src/memory/store.ts
|
|
11000
|
-
import { randomUUID as
|
|
11384
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
11001
11385
|
var MemoryStore = class {
|
|
11002
11386
|
constructor(db) {
|
|
11003
11387
|
this.db = db;
|
|
11004
11388
|
}
|
|
11005
11389
|
db;
|
|
11006
11390
|
isAutoExtractionEnabled() {
|
|
11007
|
-
const row = this.db.prepare("SELECT value FROM
|
|
11008
|
-
return row?.value
|
|
11391
|
+
const row = this.db.prepare("SELECT value FROM application_settings WHERE key = ?").get("memory.autoExtract");
|
|
11392
|
+
return row?.value === "true";
|
|
11009
11393
|
}
|
|
11010
11394
|
getOrCreateProject(profileId, canonicalPath) {
|
|
11011
11395
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -11013,7 +11397,7 @@ var MemoryStore = class {
|
|
|
11013
11397
|
INSERT INTO memory_projects (id, profile_id, canonical_path, created_at, updated_at)
|
|
11014
11398
|
VALUES (?, ?, ?, ?, ?)
|
|
11015
11399
|
ON CONFLICT(profile_id, canonical_path) DO NOTHING
|
|
11016
|
-
`).run(
|
|
11400
|
+
`).run(randomUUID5(), profileId, canonicalPath, now, now);
|
|
11017
11401
|
return this.getProjectByPath(profileId, canonicalPath);
|
|
11018
11402
|
}
|
|
11019
11403
|
getProjectById(id) {
|
|
@@ -11071,7 +11455,7 @@ var MemoryStore = class {
|
|
|
11071
11455
|
const evidence = input.evidence ? assertMemoryContent(input.evidence) : null;
|
|
11072
11456
|
const tags = normalizeMemoryTags(input.tags);
|
|
11073
11457
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11074
|
-
const id =
|
|
11458
|
+
const id = randomUUID5();
|
|
11075
11459
|
try {
|
|
11076
11460
|
this.db.prepare(`
|
|
11077
11461
|
INSERT INTO memories (
|
|
@@ -11375,7 +11759,7 @@ var MemoryStore = class {
|
|
|
11375
11759
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, 0, ?)
|
|
11376
11760
|
ON CONFLICT(source_session_id, ending_leaf_id, source_kind) DO NOTHING
|
|
11377
11761
|
`).run(
|
|
11378
|
-
|
|
11762
|
+
randomUUID5(),
|
|
11379
11763
|
input.profileId,
|
|
11380
11764
|
input.projectId,
|
|
11381
11765
|
input.sourceSessionId,
|
|
@@ -11886,7 +12270,7 @@ function createMemoryRuntime(options) {
|
|
|
11886
12270
|
// src/services/worktree-manager.ts
|
|
11887
12271
|
import { execFile as execFileCallback } from "child_process";
|
|
11888
12272
|
import { copyFile, mkdir as mkdir5, readdir as readdir3 } from "fs/promises";
|
|
11889
|
-
import { basename as basename5, dirname as dirname8, isAbsolute as
|
|
12273
|
+
import { basename as basename5, dirname as dirname8, isAbsolute as isAbsolute4, join as join13 } from "path";
|
|
11890
12274
|
import { promisify as promisify4 } from "util";
|
|
11891
12275
|
var defaultExecFile = promisify4(execFileCallback);
|
|
11892
12276
|
var WorktreeManager = class {
|
|
@@ -12008,7 +12392,7 @@ var WorktreeManager = class {
|
|
|
12008
12392
|
}
|
|
12009
12393
|
}
|
|
12010
12394
|
isRootFileName(name) {
|
|
12011
|
-
return Boolean(name.trim()) && !
|
|
12395
|
+
return Boolean(name.trim()) && !isAbsolute4(name) && !name.includes("/") && !name.includes("\\") && basename5(name) === name;
|
|
12012
12396
|
}
|
|
12013
12397
|
};
|
|
12014
12398
|
|
|
@@ -12381,8 +12765,8 @@ function matchesNoProxy(host, entry) {
|
|
|
12381
12765
|
|
|
12382
12766
|
// src/services/github-client.ts
|
|
12383
12767
|
var GithubRequestError = class extends Error {
|
|
12384
|
-
constructor(
|
|
12385
|
-
super(
|
|
12768
|
+
constructor(message2, statusCode) {
|
|
12769
|
+
super(message2);
|
|
12386
12770
|
this.statusCode = statusCode;
|
|
12387
12771
|
this.name = "GithubRequestError";
|
|
12388
12772
|
}
|
|
@@ -12481,8 +12865,8 @@ function execFileCapture(file, args, options) {
|
|
|
12481
12865
|
function parseResponse2(text, ok, status) {
|
|
12482
12866
|
const data = parseJsonObject3(text);
|
|
12483
12867
|
if (!ok) {
|
|
12484
|
-
const
|
|
12485
|
-
throw new GithubRequestError(
|
|
12868
|
+
const message2 = typeof data.message === "string" ? data.message : `GitHub request failed (${status})`;
|
|
12869
|
+
throw new GithubRequestError(message2, status);
|
|
12486
12870
|
}
|
|
12487
12871
|
return data;
|
|
12488
12872
|
}
|
|
@@ -12556,18 +12940,18 @@ var GatewaySettingsStore = class {
|
|
|
12556
12940
|
else this.delete(key);
|
|
12557
12941
|
}
|
|
12558
12942
|
value(key) {
|
|
12559
|
-
const row = this.db.prepare("SELECT value FROM
|
|
12943
|
+
const row = this.db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
|
|
12560
12944
|
return row?.value || "";
|
|
12561
12945
|
}
|
|
12562
12946
|
set(key, value) {
|
|
12563
12947
|
this.db.prepare(`
|
|
12564
|
-
INSERT INTO
|
|
12948
|
+
INSERT INTO application_settings (key, value, updated_at)
|
|
12565
12949
|
VALUES (?, ?, ?)
|
|
12566
12950
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
12567
12951
|
`).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
|
|
12568
12952
|
}
|
|
12569
12953
|
delete(key) {
|
|
12570
|
-
this.db.prepare("DELETE FROM
|
|
12954
|
+
this.db.prepare("DELETE FROM application_settings WHERE key = ?").run(key);
|
|
12571
12955
|
}
|
|
12572
12956
|
};
|
|
12573
12957
|
function normalizeCwdList(value) {
|
|
@@ -12689,8 +13073,8 @@ function toSessionOptions(task, cwd) {
|
|
|
12689
13073
|
|
|
12690
13074
|
// src/services/repository-cloner.ts
|
|
12691
13075
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
12692
|
-
import { randomUUID as
|
|
12693
|
-
import { rm as rm4, stat as
|
|
13076
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
13077
|
+
import { rm as rm4, stat as stat6 } from "node:fs/promises";
|
|
12694
13078
|
import { join as join14 } from "node:path";
|
|
12695
13079
|
var GIT_PROGRESS_PATTERNS = [
|
|
12696
13080
|
{ pattern: /^Receiving objects:\s+(\d+)%/, status: "Receiving objects\u2026" },
|
|
@@ -12710,7 +13094,7 @@ var RepositoryCloner = class {
|
|
|
12710
13094
|
if (!destinationPath) throw new Error("Destination path is required");
|
|
12711
13095
|
const pathExists = this.options.pathExists || defaultPathExists;
|
|
12712
13096
|
if (await pathExists(destinationPath)) return { status: "destination_exists", existingPath: destinationPath };
|
|
12713
|
-
const job = new CloneJob(`clone_${
|
|
13097
|
+
const job = new CloneJob(`clone_${randomUUID6()}`, remoteUrl, destinationPath, input.shallow === true);
|
|
12714
13098
|
this.jobs.set(job.id, job);
|
|
12715
13099
|
await this.spawnClone(job, remoteUrl, false, input.clientId);
|
|
12716
13100
|
return { status: "started", jobId: job.id };
|
|
@@ -12869,7 +13253,7 @@ function withToken(remoteUrl, username, token) {
|
|
|
12869
13253
|
return parsed.toString();
|
|
12870
13254
|
}
|
|
12871
13255
|
async function defaultPathExists(path13) {
|
|
12872
|
-
return
|
|
13256
|
+
return stat6(path13).then(() => true, () => false);
|
|
12873
13257
|
}
|
|
12874
13258
|
async function defaultRemovePath(path13) {
|
|
12875
13259
|
await rm4(path13, { recursive: true, force: true });
|
|
@@ -12877,8 +13261,8 @@ async function defaultRemovePath(path13) {
|
|
|
12877
13261
|
|
|
12878
13262
|
// src/services/feishu-gateway.ts
|
|
12879
13263
|
import { createDecipheriv, createHash as createHash7 } from "node:crypto";
|
|
12880
|
-
import { stat as
|
|
12881
|
-
import { isAbsolute as
|
|
13264
|
+
import { stat as stat7 } from "node:fs/promises";
|
|
13265
|
+
import { isAbsolute as isAbsolute5 } from "node:path";
|
|
12882
13266
|
|
|
12883
13267
|
// src/services/gateway-command-aliases.ts
|
|
12884
13268
|
var DEFAULT_COMMAND_PREFIX = "\u5C0F\u52A9\u7406";
|
|
@@ -12964,12 +13348,12 @@ var FeishuGatewayService = class {
|
|
|
12964
13348
|
if (payload.type === "url_verification") {
|
|
12965
13349
|
return { challenge: stringValue(payload.challenge) };
|
|
12966
13350
|
}
|
|
12967
|
-
const
|
|
12968
|
-
if (!
|
|
12969
|
-
if (this.markDuplicate(
|
|
13351
|
+
const message2 = this.parseMessage(payload);
|
|
13352
|
+
if (!message2) return { ok: true, ignored: true };
|
|
13353
|
+
if (this.markDuplicate(message2.eventId) || this.markDuplicate(message2.messageId)) {
|
|
12970
13354
|
return { ok: true, duplicate: true };
|
|
12971
13355
|
}
|
|
12972
|
-
this.enqueue(
|
|
13356
|
+
this.enqueue(message2, config);
|
|
12973
13357
|
return { ok: true };
|
|
12974
13358
|
}
|
|
12975
13359
|
loadConfig() {
|
|
@@ -13041,22 +13425,22 @@ var FeishuGatewayService = class {
|
|
|
13041
13425
|
const event = asRecord(payload.event);
|
|
13042
13426
|
const eventType = stringValue(header.event_type);
|
|
13043
13427
|
if (eventType && eventType !== "im.message.receive_v1") return null;
|
|
13044
|
-
const
|
|
13045
|
-
const messageType = stringValue(
|
|
13428
|
+
const message2 = asRecord(event.message);
|
|
13429
|
+
const messageType = stringValue(message2.message_type);
|
|
13046
13430
|
if (messageType && messageType !== "text") {
|
|
13047
|
-
logUnsupportedFeishuMessage(
|
|
13431
|
+
logUnsupportedFeishuMessage(message2, messageType);
|
|
13048
13432
|
return null;
|
|
13049
13433
|
}
|
|
13050
|
-
const text = parseFeishuTextContent(stringValue(
|
|
13051
|
-
const chatId = stringValue(
|
|
13052
|
-
const messageId = stringValue(
|
|
13434
|
+
const text = parseFeishuTextContent(stringValue(message2.content)).trim();
|
|
13435
|
+
const chatId = stringValue(message2.chat_id);
|
|
13436
|
+
const messageId = stringValue(message2.message_id);
|
|
13053
13437
|
if (!text || !chatId || !messageId) return null;
|
|
13054
13438
|
return {
|
|
13055
13439
|
eventId: stringValue(header.event_id) || messageId,
|
|
13056
13440
|
messageId,
|
|
13057
13441
|
chatId,
|
|
13058
|
-
chatType: stringValue(
|
|
13059
|
-
threadId: stringValue(
|
|
13442
|
+
chatType: stringValue(message2.chat_type) || "p2p",
|
|
13443
|
+
threadId: stringValue(message2.thread_id) || stringValue(message2.parent_id) || stringValue(message2.root_id),
|
|
13060
13444
|
text
|
|
13061
13445
|
};
|
|
13062
13446
|
}
|
|
@@ -13070,24 +13454,24 @@ var FeishuGatewayService = class {
|
|
|
13070
13454
|
this.seen.set(id, now + DEDUP_TTL_MS);
|
|
13071
13455
|
return false;
|
|
13072
13456
|
}
|
|
13073
|
-
enqueue(
|
|
13074
|
-
const clientId = this.clientIdForMessage(
|
|
13457
|
+
enqueue(message2, config) {
|
|
13458
|
+
const clientId = this.clientIdForMessage(message2);
|
|
13075
13459
|
const previous = this.queues.get(clientId) || Promise.resolve();
|
|
13076
|
-
const next = previous.catch(() => void 0).then(() => this.processMessage(clientId,
|
|
13460
|
+
const next = previous.catch(() => void 0).then(() => this.processMessage(clientId, message2, config)).catch((error) => console.error("[feishu-gateway] message handling failed:", error)).finally(() => {
|
|
13077
13461
|
if (this.queues.get(clientId) === next) this.queues.delete(clientId);
|
|
13078
13462
|
});
|
|
13079
13463
|
this.queues.set(clientId, next);
|
|
13080
13464
|
}
|
|
13081
|
-
clientIdForMessage(
|
|
13082
|
-
const chatType =
|
|
13083
|
-
const thread =
|
|
13084
|
-
return `feishu:${chatType}:${
|
|
13465
|
+
clientIdForMessage(message2) {
|
|
13466
|
+
const chatType = message2.chatType === "p2p" ? "dm" : message2.chatType || "chat";
|
|
13467
|
+
const thread = message2.threadId ? `:${message2.threadId}` : "";
|
|
13468
|
+
return `feishu:${chatType}:${message2.chatId}${thread}`;
|
|
13085
13469
|
}
|
|
13086
|
-
async processMessage(clientId,
|
|
13470
|
+
async processMessage(clientId, message2, config) {
|
|
13087
13471
|
const effectiveConfig = this.resolveChatConfig(clientId, config);
|
|
13088
|
-
if (await this.handleCommand(clientId,
|
|
13472
|
+
if (await this.handleCommand(clientId, message2, config, effectiveConfig)) return;
|
|
13089
13473
|
if (!effectiveConfig.defaultCwd) {
|
|
13090
|
-
await this.sendReply(config,
|
|
13474
|
+
await this.sendReply(config, message2.messageId, GATEWAY_FOLDERS_REQUIRED_MESSAGE);
|
|
13091
13475
|
return;
|
|
13092
13476
|
}
|
|
13093
13477
|
const session = await this.ensureSession(clientId, effectiveConfig);
|
|
@@ -13101,13 +13485,13 @@ var FeishuGatewayService = class {
|
|
|
13101
13485
|
});
|
|
13102
13486
|
try {
|
|
13103
13487
|
await this.sessionService.runForegroundWithClientProfileProxy(clientId, async () => {
|
|
13104
|
-
await session.prompt(
|
|
13488
|
+
await session.prompt(message2.text);
|
|
13105
13489
|
});
|
|
13106
13490
|
} finally {
|
|
13107
13491
|
unsubscribe();
|
|
13108
13492
|
}
|
|
13109
13493
|
const response = chunks.join("").trim() || extractLastAssistantText(session.messages) || "(Pi finished without a text response.)";
|
|
13110
|
-
await this.sendReply(config,
|
|
13494
|
+
await this.sendReply(config, message2.messageId, response);
|
|
13111
13495
|
}
|
|
13112
13496
|
async ensureSession(clientId, config) {
|
|
13113
13497
|
await this.sessionService.setClientAgentProfile(clientId, config.agentProfile || "default");
|
|
@@ -13122,8 +13506,8 @@ var FeishuGatewayService = class {
|
|
|
13122
13506
|
this.saveSessionMapping(clientId, result.session.sessionId);
|
|
13123
13507
|
return result.session;
|
|
13124
13508
|
}
|
|
13125
|
-
async handleCommand(clientId,
|
|
13126
|
-
const commandText = normalizeGatewayCommandText(
|
|
13509
|
+
async handleCommand(clientId, message2, baseConfig, effectiveConfig) {
|
|
13510
|
+
const commandText = normalizeGatewayCommandText(message2.text);
|
|
13127
13511
|
const [command = "", ...args] = commandText.split(/\s+/);
|
|
13128
13512
|
const normalized = command.toLowerCase();
|
|
13129
13513
|
if (!normalized.startsWith("/")) return false;
|
|
@@ -13132,66 +13516,66 @@ var FeishuGatewayService = class {
|
|
|
13132
13516
|
this.resetSession(clientId);
|
|
13133
13517
|
await this.sendReply(
|
|
13134
13518
|
baseConfig,
|
|
13135
|
-
|
|
13519
|
+
message2.messageId,
|
|
13136
13520
|
await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Reset this Feishu chat. A fresh Pi session will start with your next message.")
|
|
13137
13521
|
);
|
|
13138
13522
|
return true;
|
|
13139
13523
|
}
|
|
13140
13524
|
if (normalized === "/help") {
|
|
13141
|
-
await this.sendReply(baseConfig,
|
|
13525
|
+
await this.sendReply(baseConfig, message2.messageId, this.formatHelp());
|
|
13142
13526
|
return true;
|
|
13143
13527
|
}
|
|
13144
13528
|
if (normalized === "/status") {
|
|
13145
|
-
await this.sendReply(baseConfig,
|
|
13529
|
+
await this.sendReply(baseConfig, message2.messageId, await this.formatStatus(clientId, effectiveConfig));
|
|
13146
13530
|
return true;
|
|
13147
13531
|
}
|
|
13148
13532
|
if (normalized === "/profiles") {
|
|
13149
|
-
await this.sendReply(baseConfig,
|
|
13533
|
+
await this.sendReply(baseConfig, message2.messageId, await this.formatProfiles(effectiveConfig.agentProfile || "default"));
|
|
13150
13534
|
return true;
|
|
13151
13535
|
}
|
|
13152
13536
|
if (normalized === "/profile" || normalized === "/use-profile" || normalized === "/set-profile") {
|
|
13153
13537
|
await this.setChatProfile(clientId, args[0]);
|
|
13154
13538
|
this.resetSession(clientId);
|
|
13155
|
-
await this.sendReply(baseConfig,
|
|
13539
|
+
await this.sendReply(baseConfig, message2.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Profile updated."));
|
|
13156
13540
|
return true;
|
|
13157
13541
|
}
|
|
13158
13542
|
if (normalized === "/cwds" || normalized === "/pwds") {
|
|
13159
|
-
await this.sendReply(baseConfig,
|
|
13543
|
+
await this.sendReply(baseConfig, message2.messageId, this.formatCwdChoices(baseConfig));
|
|
13160
13544
|
return true;
|
|
13161
13545
|
}
|
|
13162
13546
|
if (normalized === "/cwd") {
|
|
13163
13547
|
if (args.length === 0) {
|
|
13164
|
-
await this.sendReply(baseConfig,
|
|
13548
|
+
await this.sendReply(baseConfig, message2.messageId, effectiveConfig.defaultCwd || NO_GATEWAY_FOLDERS_MESSAGE);
|
|
13165
13549
|
return true;
|
|
13166
13550
|
}
|
|
13167
13551
|
await this.setChatCwd(clientId, args[0], baseConfig);
|
|
13168
13552
|
this.resetSession(clientId);
|
|
13169
|
-
await this.sendReply(baseConfig,
|
|
13553
|
+
await this.sendReply(baseConfig, message2.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Working directory updated."));
|
|
13170
13554
|
return true;
|
|
13171
13555
|
}
|
|
13172
13556
|
if (normalized === "/skillsets") {
|
|
13173
|
-
await this.sendReply(baseConfig,
|
|
13557
|
+
await this.sendReply(baseConfig, message2.messageId, this.formatSkillsets(effectiveConfig.skillPresetId));
|
|
13174
13558
|
return true;
|
|
13175
13559
|
}
|
|
13176
13560
|
if (normalized === "/skillset") {
|
|
13177
13561
|
await this.setChatSkillset(clientId, args.join(" "));
|
|
13178
13562
|
this.resetSession(clientId);
|
|
13179
|
-
await this.sendReply(baseConfig,
|
|
13563
|
+
await this.sendReply(baseConfig, message2.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Skillset updated."));
|
|
13180
13564
|
return true;
|
|
13181
13565
|
}
|
|
13182
13566
|
if (normalized === "/clear-config") {
|
|
13183
13567
|
this.clearChatConfig(clientId);
|
|
13184
13568
|
this.resetSession(clientId);
|
|
13185
|
-
await this.sendReply(baseConfig,
|
|
13569
|
+
await this.sendReply(baseConfig, message2.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Feishu chat config cleared."));
|
|
13186
13570
|
return true;
|
|
13187
13571
|
}
|
|
13188
13572
|
} catch (error) {
|
|
13189
|
-
await this.sendReply(baseConfig,
|
|
13573
|
+
await this.sendReply(baseConfig, message2.messageId, `Command failed: ${error instanceof Error ? error.message : "unknown error"}
|
|
13190
13574
|
|
|
13191
13575
|
${this.formatHelp()}`);
|
|
13192
13576
|
return true;
|
|
13193
13577
|
}
|
|
13194
|
-
await this.sendReply(baseConfig,
|
|
13578
|
+
await this.sendReply(baseConfig, message2.messageId, `Unknown command: ${command}
|
|
13195
13579
|
|
|
13196
13580
|
${this.formatHelp()}`);
|
|
13197
13581
|
return true;
|
|
@@ -13266,8 +13650,8 @@ ${this.formatHelp()}`);
|
|
|
13266
13650
|
const slot = Number(slotText.trim());
|
|
13267
13651
|
if (!Number.isInteger(slot) || slot < 1 || slot > config.cwdChoices.length) throw new Error(`Usage: /cwd <1-${config.cwdChoices.length}>`);
|
|
13268
13652
|
const target = config.cwdChoices[slot - 1];
|
|
13269
|
-
if (!
|
|
13270
|
-
const info = await
|
|
13653
|
+
if (!isAbsolute5(target)) throw new Error(`Configured CWD ${slot} must be an absolute path`);
|
|
13654
|
+
const info = await stat7(target);
|
|
13271
13655
|
if (!info.isDirectory()) throw new Error(`Configured CWD ${slot} must be an existing directory`);
|
|
13272
13656
|
this.saveChatConfig(clientId, { defaultCwd: target });
|
|
13273
13657
|
}
|
|
@@ -13428,13 +13812,13 @@ function parseFeishuTextContent(content) {
|
|
|
13428
13812
|
return content;
|
|
13429
13813
|
}
|
|
13430
13814
|
}
|
|
13431
|
-
function logUnsupportedFeishuMessage(
|
|
13432
|
-
const content = parseJsonRecord(stringValue(
|
|
13815
|
+
function logUnsupportedFeishuMessage(message2, messageType) {
|
|
13816
|
+
const content = parseJsonRecord(stringValue(message2.content));
|
|
13433
13817
|
console.info("[feishu-gateway] unsupported inbound message", {
|
|
13434
13818
|
messageType,
|
|
13435
|
-
messageId: stringValue(
|
|
13436
|
-
chatType: stringValue(
|
|
13437
|
-
messageKeys: Object.keys(
|
|
13819
|
+
messageId: stringValue(message2.message_id) || void 0,
|
|
13820
|
+
chatType: stringValue(message2.chat_type) || void 0,
|
|
13821
|
+
messageKeys: Object.keys(message2).sort(),
|
|
13438
13822
|
contentKeys: Object.keys(content).sort(),
|
|
13439
13823
|
contentStringFields: Object.fromEntries(
|
|
13440
13824
|
Object.entries(content).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value.length])
|
|
@@ -13458,9 +13842,9 @@ function truncateFeishuText(text) {
|
|
|
13458
13842
|
function extractLastAssistantText(messages) {
|
|
13459
13843
|
if (!Array.isArray(messages)) return "";
|
|
13460
13844
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
13461
|
-
const
|
|
13462
|
-
if (
|
|
13463
|
-
const content =
|
|
13845
|
+
const message2 = messages[index];
|
|
13846
|
+
if (message2?.role !== "assistant") continue;
|
|
13847
|
+
const content = message2.content;
|
|
13464
13848
|
if (typeof content === "string") return content.trim();
|
|
13465
13849
|
if (Array.isArray(content)) {
|
|
13466
13850
|
const text = content.map((part) => typeof part === "string" ? part : part?.text || part?.content || "").join("").trim();
|
|
@@ -13471,9 +13855,9 @@ function extractLastAssistantText(messages) {
|
|
|
13471
13855
|
}
|
|
13472
13856
|
|
|
13473
13857
|
// src/services/weixin-gateway.ts
|
|
13474
|
-
import { randomUUID as
|
|
13475
|
-
import { stat as
|
|
13476
|
-
import { isAbsolute as
|
|
13858
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
13859
|
+
import { stat as stat8 } from "node:fs/promises";
|
|
13860
|
+
import { isAbsolute as isAbsolute6 } from "node:path";
|
|
13477
13861
|
import QRCode from "qrcode";
|
|
13478
13862
|
import { Agent, fetch as undiciFetch2 } from "undici";
|
|
13479
13863
|
|
|
@@ -13734,11 +14118,11 @@ var WeixinGatewayService = class {
|
|
|
13734
14118
|
}
|
|
13735
14119
|
const messages = Array.isArray(response.msgs) ? response.msgs : [];
|
|
13736
14120
|
for (const raw of messages) {
|
|
13737
|
-
const
|
|
13738
|
-
if (!
|
|
13739
|
-
if (this.markDuplicate(
|
|
13740
|
-
if (
|
|
13741
|
-
this.enqueue(
|
|
14121
|
+
const message2 = await this.parseMessage(asRecord2(raw), config);
|
|
14122
|
+
if (!message2) continue;
|
|
14123
|
+
if (this.markDuplicate(message2.messageId)) continue;
|
|
14124
|
+
if (message2.contextToken) this.saveContextToken(config.accountId, message2.senderId, message2.contextToken);
|
|
14125
|
+
this.enqueue(message2, config);
|
|
13742
14126
|
}
|
|
13743
14127
|
} catch (error) {
|
|
13744
14128
|
if (!this.running) break;
|
|
@@ -13750,17 +14134,17 @@ var WeixinGatewayService = class {
|
|
|
13750
14134
|
}
|
|
13751
14135
|
console.info("[weixin-gateway] stopped");
|
|
13752
14136
|
}
|
|
13753
|
-
async parseMessage(
|
|
13754
|
-
const senderId = stringValue2(
|
|
14137
|
+
async parseMessage(message2, config) {
|
|
14138
|
+
const senderId = stringValue2(message2.from_user_id);
|
|
13755
14139
|
if (!senderId || senderId === config.accountId) return null;
|
|
13756
|
-
const { chatType, chatId } = this.guessChat(
|
|
14140
|
+
const { chatType, chatId } = this.guessChat(message2, config.accountId);
|
|
13757
14141
|
if (chatType === "group") return null;
|
|
13758
14142
|
if (!this.isDmAllowed(senderId, config)) return null;
|
|
13759
|
-
const items = Array.isArray(
|
|
14143
|
+
const items = Array.isArray(message2.item_list) ? message2.item_list.map(asRecord2) : [];
|
|
13760
14144
|
const text = extractText2(items).trim();
|
|
13761
14145
|
const imageResult = await this.extractImages(config, items);
|
|
13762
|
-
const messageId = stringValue2(
|
|
13763
|
-
if (!text && imageResult.images.length === 0 && !imageResult.error) this.logUnsupportedInboundMessage(
|
|
14146
|
+
const messageId = stringValue2(message2.message_id) || randomUUID7();
|
|
14147
|
+
if (!text && imageResult.images.length === 0 && !imageResult.error) this.logUnsupportedInboundMessage(message2, items, "no text or supported image item");
|
|
13764
14148
|
if (!text && imageResult.images.length === 0 && !imageResult.error || !chatId) return null;
|
|
13765
14149
|
return {
|
|
13766
14150
|
messageId,
|
|
@@ -13770,15 +14154,15 @@ var WeixinGatewayService = class {
|
|
|
13770
14154
|
text,
|
|
13771
14155
|
images: imageResult.images,
|
|
13772
14156
|
imageError: imageResult.error,
|
|
13773
|
-
contextToken: stringValue2(
|
|
14157
|
+
contextToken: stringValue2(message2.context_token) || void 0
|
|
13774
14158
|
};
|
|
13775
14159
|
}
|
|
13776
|
-
guessChat(
|
|
13777
|
-
const roomId = stringValue2(
|
|
13778
|
-
const toUserId = stringValue2(
|
|
13779
|
-
const isGroup = Boolean(roomId) || Boolean(toUserId && accountId && toUserId !== accountId && Number(
|
|
13780
|
-
if (isGroup) return { chatType: "group", chatId: roomId || toUserId || stringValue2(
|
|
13781
|
-
return { chatType: "dm", chatId: stringValue2(
|
|
14160
|
+
guessChat(message2, accountId) {
|
|
14161
|
+
const roomId = stringValue2(message2.room_id) || stringValue2(message2.chat_room_id);
|
|
14162
|
+
const toUserId = stringValue2(message2.to_user_id);
|
|
14163
|
+
const isGroup = Boolean(roomId) || Boolean(toUserId && accountId && toUserId !== accountId && Number(message2.msg_type) === 1);
|
|
14164
|
+
if (isGroup) return { chatType: "group", chatId: roomId || toUserId || stringValue2(message2.from_user_id) };
|
|
14165
|
+
return { chatType: "dm", chatId: stringValue2(message2.from_user_id) };
|
|
13782
14166
|
}
|
|
13783
14167
|
isDmAllowed(senderId, config) {
|
|
13784
14168
|
if (config.dmPolicy === "disabled") return false;
|
|
@@ -13786,13 +14170,13 @@ var WeixinGatewayService = class {
|
|
|
13786
14170
|
if (config.dmPolicy === "open") return parseBoolean3(process.env.PI_CLOUD_WECHAT_ALLOW_ALL_USERS || process.env.GATEWAY_ALLOW_ALL_USERS, false);
|
|
13787
14171
|
return true;
|
|
13788
14172
|
}
|
|
13789
|
-
logUnsupportedInboundMessage(
|
|
14173
|
+
logUnsupportedInboundMessage(message2, items, reason) {
|
|
13790
14174
|
console.info("[weixin-gateway] unsupported inbound message", JSON.stringify({
|
|
13791
14175
|
reason,
|
|
13792
|
-
messageId:
|
|
13793
|
-
msgType:
|
|
13794
|
-
messageType:
|
|
13795
|
-
messageKeys: Object.keys(
|
|
14176
|
+
messageId: message2.message_id,
|
|
14177
|
+
msgType: message2.msg_type,
|
|
14178
|
+
messageType: message2.message_type,
|
|
14179
|
+
messageKeys: Object.keys(message2).sort(),
|
|
13796
14180
|
itemSummaries: items.map(summarizeWeixinItem)
|
|
13797
14181
|
}, null, 2));
|
|
13798
14182
|
}
|
|
@@ -13880,36 +14264,36 @@ var WeixinGatewayService = class {
|
|
|
13880
14264
|
this.seen.set(id, now + DEDUP_TTL_MS2);
|
|
13881
14265
|
return false;
|
|
13882
14266
|
}
|
|
13883
|
-
enqueue(
|
|
13884
|
-
const clientId = this.clientIdForMessage(
|
|
14267
|
+
enqueue(message2, config) {
|
|
14268
|
+
const clientId = this.clientIdForMessage(message2);
|
|
13885
14269
|
const previous = this.queues.get(clientId) || Promise.resolve();
|
|
13886
|
-
const next = previous.catch(() => void 0).then(() => this.processMessage(clientId,
|
|
14270
|
+
const next = previous.catch(() => void 0).then(() => this.processMessage(clientId, message2, config)).catch((error) => console.error("[weixin-gateway] message handling failed:", error)).finally(() => {
|
|
13887
14271
|
if (this.queues.get(clientId) === next) this.queues.delete(clientId);
|
|
13888
14272
|
});
|
|
13889
14273
|
this.queues.set(clientId, next);
|
|
13890
14274
|
}
|
|
13891
|
-
clientIdForMessage(
|
|
13892
|
-
return `weixin:${
|
|
14275
|
+
clientIdForMessage(message2) {
|
|
14276
|
+
return `weixin:${message2.chatType}:${message2.chatId}`;
|
|
13893
14277
|
}
|
|
13894
|
-
async processMessage(clientId,
|
|
14278
|
+
async processMessage(clientId, message2, config) {
|
|
13895
14279
|
const effectiveConfig = this.resolveChatConfig(clientId, config);
|
|
13896
|
-
if (await this.handleCommand(clientId,
|
|
14280
|
+
if (await this.handleCommand(clientId, message2, config, effectiveConfig)) return;
|
|
13897
14281
|
if (!effectiveConfig.defaultCwd) {
|
|
13898
|
-
await this.sendReply(config,
|
|
14282
|
+
await this.sendReply(config, message2.chatId, GATEWAY_FOLDERS_REQUIRED_MESSAGE2);
|
|
13899
14283
|
return;
|
|
13900
14284
|
}
|
|
13901
|
-
if (
|
|
13902
|
-
await this.sendReply(config,
|
|
14285
|
+
if (message2.imageError && message2.images.length === 0) {
|
|
14286
|
+
await this.sendReply(config, message2.chatId, message2.imageError);
|
|
13903
14287
|
return;
|
|
13904
14288
|
}
|
|
13905
14289
|
const session = await this.ensureSession(clientId, effectiveConfig);
|
|
13906
|
-
const imageResult = validateImages(
|
|
14290
|
+
const imageResult = validateImages(message2.images, session.model);
|
|
13907
14291
|
if (!imageResult.ok) {
|
|
13908
|
-
await this.sendReply(config,
|
|
14292
|
+
await this.sendReply(config, message2.chatId, imageResult.message);
|
|
13909
14293
|
return;
|
|
13910
14294
|
}
|
|
13911
|
-
await this.saveInboundImages(effectiveConfig.defaultCwd,
|
|
13912
|
-
const promptText =
|
|
14295
|
+
await this.saveInboundImages(effectiveConfig.defaultCwd, message2.messageId, imageResult.images);
|
|
14296
|
+
const promptText = message2.text || (imageResult.images.length ? "Please analyze the attached image." : "");
|
|
13913
14297
|
const chunks = [];
|
|
13914
14298
|
const unsubscribe = session.subscribe((event) => {
|
|
13915
14299
|
if (event?.type !== "message_update") return;
|
|
@@ -13918,7 +14302,7 @@ var WeixinGatewayService = class {
|
|
|
13918
14302
|
chunks.push(update.delta);
|
|
13919
14303
|
}
|
|
13920
14304
|
});
|
|
13921
|
-
const stopTypingIndicator = this.startTypingIndicator(config,
|
|
14305
|
+
const stopTypingIndicator = this.startTypingIndicator(config, message2.chatId);
|
|
13922
14306
|
try {
|
|
13923
14307
|
await this.sessionService.runForegroundWithClientProfileProxy(clientId, async () => {
|
|
13924
14308
|
await session.prompt(promptText, imageResult.images.length ? { images: imageResult.images } : void 0);
|
|
@@ -13928,7 +14312,7 @@ var WeixinGatewayService = class {
|
|
|
13928
14312
|
unsubscribe();
|
|
13929
14313
|
}
|
|
13930
14314
|
const response = chunks.join("").trim() || extractLastAssistantText2(session.messages) || "(Pi finished without a text response.)";
|
|
13931
|
-
await this.sendReply(config,
|
|
14315
|
+
await this.sendReply(config, message2.chatId, response);
|
|
13932
14316
|
}
|
|
13933
14317
|
async saveInboundImages(projectRoot, messageId, images) {
|
|
13934
14318
|
if (!projectRoot || images.length === 0) return;
|
|
@@ -13959,8 +14343,8 @@ var WeixinGatewayService = class {
|
|
|
13959
14343
|
this.saveSessionMapping(clientId, result.session.sessionId);
|
|
13960
14344
|
return result.session;
|
|
13961
14345
|
}
|
|
13962
|
-
async handleCommand(clientId,
|
|
13963
|
-
const commandText = normalizeGatewayCommandText(
|
|
14346
|
+
async handleCommand(clientId, message2, baseConfig, effectiveConfig) {
|
|
14347
|
+
const commandText = normalizeGatewayCommandText(message2.text);
|
|
13964
14348
|
const [command = "", ...args] = commandText.split(/\s+/);
|
|
13965
14349
|
const normalized = command.toLowerCase();
|
|
13966
14350
|
if (!normalized.startsWith("/")) return false;
|
|
@@ -13969,66 +14353,66 @@ var WeixinGatewayService = class {
|
|
|
13969
14353
|
this.resetSession(clientId);
|
|
13970
14354
|
await this.sendReply(
|
|
13971
14355
|
baseConfig,
|
|
13972
|
-
|
|
14356
|
+
message2.chatId,
|
|
13973
14357
|
await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Reset this WeChat chat. A fresh Pi session will start with your next message.")
|
|
13974
14358
|
);
|
|
13975
14359
|
return true;
|
|
13976
14360
|
}
|
|
13977
14361
|
if (normalized === "/help") {
|
|
13978
|
-
await this.sendReply(baseConfig,
|
|
14362
|
+
await this.sendReply(baseConfig, message2.chatId, this.formatHelp());
|
|
13979
14363
|
return true;
|
|
13980
14364
|
}
|
|
13981
14365
|
if (normalized === "/status") {
|
|
13982
|
-
await this.sendReply(baseConfig,
|
|
14366
|
+
await this.sendReply(baseConfig, message2.chatId, await this.formatStatus(clientId, effectiveConfig));
|
|
13983
14367
|
return true;
|
|
13984
14368
|
}
|
|
13985
14369
|
if (normalized === "/profiles") {
|
|
13986
|
-
await this.sendReply(baseConfig,
|
|
14370
|
+
await this.sendReply(baseConfig, message2.chatId, await this.formatProfiles(effectiveConfig.agentProfile || "default"));
|
|
13987
14371
|
return true;
|
|
13988
14372
|
}
|
|
13989
14373
|
if (normalized === "/profile" || normalized === "/use-profile" || normalized === "/set-profile") {
|
|
13990
14374
|
await this.setChatProfile(clientId, args[0]);
|
|
13991
14375
|
this.resetSession(clientId);
|
|
13992
|
-
await this.sendReply(baseConfig,
|
|
14376
|
+
await this.sendReply(baseConfig, message2.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Profile updated."));
|
|
13993
14377
|
return true;
|
|
13994
14378
|
}
|
|
13995
14379
|
if (normalized === "/cwds" || normalized === "/pwds") {
|
|
13996
|
-
await this.sendReply(baseConfig,
|
|
14380
|
+
await this.sendReply(baseConfig, message2.chatId, this.formatCwdChoices(baseConfig));
|
|
13997
14381
|
return true;
|
|
13998
14382
|
}
|
|
13999
14383
|
if (normalized === "/cwd") {
|
|
14000
14384
|
if (args.length === 0) {
|
|
14001
|
-
await this.sendReply(baseConfig,
|
|
14385
|
+
await this.sendReply(baseConfig, message2.chatId, effectiveConfig.defaultCwd || NO_GATEWAY_FOLDERS_MESSAGE2);
|
|
14002
14386
|
return true;
|
|
14003
14387
|
}
|
|
14004
14388
|
await this.setChatCwd(clientId, args[0], baseConfig);
|
|
14005
14389
|
this.resetSession(clientId);
|
|
14006
|
-
await this.sendReply(baseConfig,
|
|
14390
|
+
await this.sendReply(baseConfig, message2.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Working directory updated."));
|
|
14007
14391
|
return true;
|
|
14008
14392
|
}
|
|
14009
14393
|
if (normalized === "/skillsets") {
|
|
14010
|
-
await this.sendReply(baseConfig,
|
|
14394
|
+
await this.sendReply(baseConfig, message2.chatId, this.formatSkillsets(effectiveConfig.skillPresetId));
|
|
14011
14395
|
return true;
|
|
14012
14396
|
}
|
|
14013
14397
|
if (normalized === "/skillset") {
|
|
14014
14398
|
await this.setChatSkillset(clientId, args.join(" "));
|
|
14015
14399
|
this.resetSession(clientId);
|
|
14016
|
-
await this.sendReply(baseConfig,
|
|
14400
|
+
await this.sendReply(baseConfig, message2.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Skillset updated."));
|
|
14017
14401
|
return true;
|
|
14018
14402
|
}
|
|
14019
14403
|
if (normalized === "/clear-config") {
|
|
14020
14404
|
this.clearChatConfig(clientId);
|
|
14021
14405
|
this.resetSession(clientId);
|
|
14022
|
-
await this.sendReply(baseConfig,
|
|
14406
|
+
await this.sendReply(baseConfig, message2.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "WeChat chat config cleared."));
|
|
14023
14407
|
return true;
|
|
14024
14408
|
}
|
|
14025
14409
|
} catch (error) {
|
|
14026
|
-
await this.sendReply(baseConfig,
|
|
14410
|
+
await this.sendReply(baseConfig, message2.chatId, `Command failed: ${error instanceof Error ? error.message : "unknown error"}
|
|
14027
14411
|
|
|
14028
14412
|
${this.formatHelp()}`);
|
|
14029
14413
|
return true;
|
|
14030
14414
|
}
|
|
14031
|
-
await this.sendReply(baseConfig,
|
|
14415
|
+
await this.sendReply(baseConfig, message2.chatId, `Unknown command: ${command}
|
|
14032
14416
|
|
|
14033
14417
|
${this.formatHelp()}`);
|
|
14034
14418
|
return true;
|
|
@@ -14103,8 +14487,8 @@ ${this.formatHelp()}`);
|
|
|
14103
14487
|
const slot = Number(slotText.trim());
|
|
14104
14488
|
if (!Number.isInteger(slot) || slot < 1 || slot > config.cwdChoices.length) throw new Error(`Usage: /cwd <1-${config.cwdChoices.length}>`);
|
|
14105
14489
|
const target = config.cwdChoices[slot - 1];
|
|
14106
|
-
if (!
|
|
14107
|
-
const info = await
|
|
14490
|
+
if (!isAbsolute6(target)) throw new Error(`Configured CWD ${slot} must be an absolute path`);
|
|
14491
|
+
const info = await stat8(target);
|
|
14108
14492
|
if (!info.isDirectory()) throw new Error(`Configured CWD ${slot} must be an existing directory`);
|
|
14109
14493
|
this.saveChatConfig(clientId, { defaultCwd: target });
|
|
14110
14494
|
}
|
|
@@ -14275,7 +14659,7 @@ ${this.formatHelp()}`);
|
|
|
14275
14659
|
msg: {
|
|
14276
14660
|
from_user_id: "",
|
|
14277
14661
|
to_user_id: chatId,
|
|
14278
|
-
client_id:
|
|
14662
|
+
client_id: randomUUID7(),
|
|
14279
14663
|
message_type: MSG_TYPE_BOT,
|
|
14280
14664
|
...msg,
|
|
14281
14665
|
...withContextToken(this.getContextToken(config.accountId, chatId))
|
|
@@ -14520,11 +14904,11 @@ function safeId(value) {
|
|
|
14520
14904
|
}
|
|
14521
14905
|
function extractLastAssistantText2(messages) {
|
|
14522
14906
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
14523
|
-
const
|
|
14524
|
-
if (
|
|
14525
|
-
if (typeof
|
|
14526
|
-
if (Array.isArray(
|
|
14527
|
-
const text =
|
|
14907
|
+
const message2 = messages[index];
|
|
14908
|
+
if (message2?.role !== "assistant") continue;
|
|
14909
|
+
if (typeof message2.content === "string") return message2.content.trim();
|
|
14910
|
+
if (Array.isArray(message2.content)) {
|
|
14911
|
+
const text = message2.content.map((part) => typeof part === "string" ? part : part?.text || part?.content || "").join("").trim();
|
|
14528
14912
|
if (text) return text;
|
|
14529
14913
|
}
|
|
14530
14914
|
}
|
|
@@ -14533,8 +14917,8 @@ function extractLastAssistantText2(messages) {
|
|
|
14533
14917
|
|
|
14534
14918
|
// src/services/wecom-gateway.ts
|
|
14535
14919
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
14536
|
-
import { stat as
|
|
14537
|
-
import { isAbsolute as
|
|
14920
|
+
import { stat as stat9 } from "node:fs/promises";
|
|
14921
|
+
import { isAbsolute as isAbsolute7 } from "node:path";
|
|
14538
14922
|
|
|
14539
14923
|
// src/services/wecom-crypto.ts
|
|
14540
14924
|
import { createDecipheriv as createDecipheriv3, createHash as createHash8, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
@@ -14682,24 +15066,24 @@ var WecomGatewayService = class {
|
|
|
14682
15066
|
const encrypted = xmlValue(body, "Encrypt");
|
|
14683
15067
|
if (!encrypted) throw gatewayError("Invalid WeCom callback envelope", 400);
|
|
14684
15068
|
this.verifySignature(query, encrypted, config);
|
|
14685
|
-
const
|
|
15069
|
+
const message2 = parseWecomMessage(decryptWecomPayload(encrypted, config.encodingAesKey, config.corpId));
|
|
14686
15070
|
this.markCallbackVerified();
|
|
14687
|
-
if (!
|
|
14688
|
-
if (config.allowedUsers.length && !config.allowedUsers.includes(
|
|
14689
|
-
if (this.markDuplicate(
|
|
14690
|
-
if (!["text", "image", "voice"].includes(
|
|
14691
|
-
void this.sendReply(config,
|
|
15071
|
+
if (!message2 || message2.agentId !== config.agentId) return "success";
|
|
15072
|
+
if (config.allowedUsers.length && !config.allowedUsers.includes(message2.userId)) return "success";
|
|
15073
|
+
if (this.markDuplicate(message2.messageId)) return "success";
|
|
15074
|
+
if (!["text", "image", "voice"].includes(message2.messageType)) {
|
|
15075
|
+
void this.sendReply(config, message2.userId, "This WeCom gateway supports text, image, and voice messages only.").catch((error) => {
|
|
14692
15076
|
console.warn("[wecom-gateway] unsupported-message reply failed:", error instanceof Error ? error.message : error);
|
|
14693
15077
|
});
|
|
14694
15078
|
return "success";
|
|
14695
15079
|
}
|
|
14696
15080
|
this.enqueue({
|
|
14697
|
-
messageId:
|
|
14698
|
-
userId:
|
|
14699
|
-
text:
|
|
14700
|
-
messageType:
|
|
14701
|
-
mediaId:
|
|
14702
|
-
picUrl:
|
|
15081
|
+
messageId: message2.messageId,
|
|
15082
|
+
userId: message2.userId,
|
|
15083
|
+
text: message2.text,
|
|
15084
|
+
messageType: message2.messageType,
|
|
15085
|
+
mediaId: message2.mediaId,
|
|
15086
|
+
picUrl: message2.picUrl
|
|
14703
15087
|
}, config);
|
|
14704
15088
|
return "success";
|
|
14705
15089
|
}
|
|
@@ -14786,57 +15170,57 @@ var WecomGatewayService = class {
|
|
|
14786
15170
|
this.seen.set(id, now + DEDUP_TTL_MS3);
|
|
14787
15171
|
return false;
|
|
14788
15172
|
}
|
|
14789
|
-
enqueue(
|
|
14790
|
-
const clientId = `wecom:dm:${
|
|
15173
|
+
enqueue(message2, config) {
|
|
15174
|
+
const clientId = `wecom:dm:${message2.userId}`;
|
|
14791
15175
|
const previous = this.queues.get(clientId) || Promise.resolve();
|
|
14792
|
-
const next = previous.catch(() => void 0).then(() => this.processMessage(clientId,
|
|
15176
|
+
const next = previous.catch(() => void 0).then(() => this.processMessage(clientId, message2, config)).catch((error) => console.error("[wecom-gateway] message handling failed:", error)).finally(() => {
|
|
14793
15177
|
if (this.queues.get(clientId) === next) this.queues.delete(clientId);
|
|
14794
15178
|
});
|
|
14795
15179
|
this.queues.set(clientId, next);
|
|
14796
15180
|
}
|
|
14797
|
-
async processMessage(clientId,
|
|
15181
|
+
async processMessage(clientId, message2, config) {
|
|
14798
15182
|
const effectiveConfig = this.resolveChatConfig(clientId, config);
|
|
14799
|
-
if (await this.handleCommand(clientId,
|
|
15183
|
+
if (await this.handleCommand(clientId, message2, config, effectiveConfig)) return;
|
|
14800
15184
|
if (!effectiveConfig.defaultCwd) {
|
|
14801
|
-
await this.sendReply(config,
|
|
15185
|
+
await this.sendReply(config, message2.userId, "No gateway folders configured. Add at least one allowed folder in Web UI Settings > Gateway.");
|
|
14802
15186
|
return;
|
|
14803
15187
|
}
|
|
14804
|
-
let promptText =
|
|
14805
|
-
if (
|
|
14806
|
-
if (!
|
|
14807
|
-
await this.sendReply(config,
|
|
15188
|
+
let promptText = message2.text;
|
|
15189
|
+
if (message2.messageType === "voice") {
|
|
15190
|
+
if (!message2.mediaId) {
|
|
15191
|
+
await this.sendReply(config, message2.userId, "The WeCom voice message did not include downloadable media. Try sending it again.");
|
|
14808
15192
|
return;
|
|
14809
15193
|
}
|
|
14810
15194
|
try {
|
|
14811
|
-
promptText = (await transcribeAudio(await this.downloadVoice(config,
|
|
15195
|
+
promptText = (await transcribeAudio(await this.downloadVoice(config, message2.mediaId), "audio/amr")).trim();
|
|
14812
15196
|
} catch (error) {
|
|
14813
15197
|
console.warn("[wecom-gateway] failed to transcribe inbound voice:", error instanceof Error ? error.message : error);
|
|
14814
|
-
await this.sendReply(config,
|
|
15198
|
+
await this.sendReply(config, message2.userId, "The voice message could not be transcribed. Check the speech-to-text configuration and try again.");
|
|
14815
15199
|
return;
|
|
14816
15200
|
}
|
|
14817
15201
|
if (!promptText) {
|
|
14818
|
-
await this.sendReply(config,
|
|
15202
|
+
await this.sendReply(config, message2.userId, "The voice message did not contain recognizable speech. Try sending it again.");
|
|
14819
15203
|
return;
|
|
14820
15204
|
}
|
|
14821
15205
|
}
|
|
14822
15206
|
const session = await this.ensureSession(clientId, effectiveConfig);
|
|
14823
15207
|
let images = [];
|
|
14824
|
-
if (
|
|
14825
|
-
if (!
|
|
14826
|
-
await this.sendReply(config,
|
|
15208
|
+
if (message2.messageType === "image") {
|
|
15209
|
+
if (!message2.mediaId) {
|
|
15210
|
+
await this.sendReply(config, message2.userId, "The WeCom image message did not include downloadable media. Try sending it again.");
|
|
14827
15211
|
return;
|
|
14828
15212
|
}
|
|
14829
15213
|
try {
|
|
14830
|
-
images = [await this.downloadImage(config,
|
|
15214
|
+
images = [await this.downloadImage(config, message2.mediaId)];
|
|
14831
15215
|
} catch (error) {
|
|
14832
15216
|
console.warn("[wecom-gateway] failed to download inbound image:", error instanceof Error ? error.message : error);
|
|
14833
|
-
await this.sendReply(config,
|
|
15217
|
+
await this.sendReply(config, message2.userId, "The image could not be downloaded from WeCom. Try sending it again.");
|
|
14834
15218
|
return;
|
|
14835
15219
|
}
|
|
14836
15220
|
}
|
|
14837
15221
|
const imageResult = validateImages(images, session.model);
|
|
14838
15222
|
if (!imageResult.ok) {
|
|
14839
|
-
await this.sendReply(config,
|
|
15223
|
+
await this.sendReply(config, message2.userId, imageResult.message);
|
|
14840
15224
|
return;
|
|
14841
15225
|
}
|
|
14842
15226
|
promptText ||= imageResult.images.length ? "Please analyze the attached image." : "";
|
|
@@ -14851,7 +15235,7 @@ var WecomGatewayService = class {
|
|
|
14851
15235
|
unsubscribe();
|
|
14852
15236
|
}
|
|
14853
15237
|
const response = chunks.join("").trim() || extractLastAssistantText3(session.messages) || "(Pi finished without a text response.)";
|
|
14854
|
-
await this.sendReply(config,
|
|
15238
|
+
await this.sendReply(config, message2.userId, response);
|
|
14855
15239
|
}
|
|
14856
15240
|
async ensureSession(clientId, config) {
|
|
14857
15241
|
await this.sessionService.setClientAgentProfile(clientId, config.agentProfile || "default");
|
|
@@ -14880,28 +15264,28 @@ var WecomGatewayService = class {
|
|
|
14880
15264
|
`).run(clientId, clientId, result.session.sessionId, now, now);
|
|
14881
15265
|
return result.session;
|
|
14882
15266
|
}
|
|
14883
|
-
async handleCommand(clientId,
|
|
14884
|
-
const commandText = normalizeGatewayCommandText(
|
|
15267
|
+
async handleCommand(clientId, message2, baseConfig, effectiveConfig) {
|
|
15268
|
+
const commandText = normalizeGatewayCommandText(message2.text);
|
|
14885
15269
|
const [command = "", ...args] = commandText.split(/\s+/);
|
|
14886
15270
|
const normalized = command.toLowerCase();
|
|
14887
15271
|
if (!normalized.startsWith("/")) return false;
|
|
14888
15272
|
try {
|
|
14889
15273
|
if (normalized === "/new" || normalized === "/reset") {
|
|
14890
15274
|
this.resetSession(clientId);
|
|
14891
|
-
await this.sendReply(baseConfig,
|
|
15275
|
+
await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Reset this WeCom chat. A fresh Pi session will start with your next message."));
|
|
14892
15276
|
return true;
|
|
14893
15277
|
}
|
|
14894
15278
|
if (normalized === "/help") {
|
|
14895
|
-
await this.sendReply(baseConfig,
|
|
15279
|
+
await this.sendReply(baseConfig, message2.userId, this.formatHelp());
|
|
14896
15280
|
return true;
|
|
14897
15281
|
}
|
|
14898
15282
|
if (normalized === "/status") {
|
|
14899
|
-
await this.sendReply(baseConfig,
|
|
15283
|
+
await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, effectiveConfig));
|
|
14900
15284
|
return true;
|
|
14901
15285
|
}
|
|
14902
15286
|
if (normalized === "/profiles") {
|
|
14903
15287
|
const profiles = await this.sessionService.listAgentProfiles();
|
|
14904
|
-
await this.sendReply(baseConfig,
|
|
15288
|
+
await this.sendReply(baseConfig, message2.userId, ["Available profiles:", ...profiles.map((profile) => `${profile.id === (effectiveConfig.agentProfile || "default") ? "*" : " "} ${profile.id}`)].join("\n"));
|
|
14905
15289
|
return true;
|
|
14906
15290
|
}
|
|
14907
15291
|
if (normalized === "/profile" || normalized === "/use-profile" || normalized === "/set-profile") {
|
|
@@ -14911,25 +15295,25 @@ var WecomGatewayService = class {
|
|
|
14911
15295
|
if (!profiles.some((item) => item.id === profile)) throw new Error(`Unknown profile: ${profile}`);
|
|
14912
15296
|
this.saveChatConfig(clientId, { agentProfile: profile });
|
|
14913
15297
|
this.resetSession(clientId);
|
|
14914
|
-
await this.sendReply(baseConfig,
|
|
15298
|
+
await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Profile updated."));
|
|
14915
15299
|
return true;
|
|
14916
15300
|
}
|
|
14917
15301
|
if (normalized === "/cwds" || normalized === "/pwds") {
|
|
14918
|
-
await this.sendReply(baseConfig,
|
|
15302
|
+
await this.sendReply(baseConfig, message2.userId, baseConfig.cwdChoices.length ? baseConfig.cwdChoices.map((cwd, index) => `${index + 1}: ${cwd}`).join("\n") : "No gateway folders configured.");
|
|
14919
15303
|
return true;
|
|
14920
15304
|
}
|
|
14921
15305
|
if (normalized === "/cwd") {
|
|
14922
15306
|
const slot = Number(args[0]);
|
|
14923
15307
|
if (!Number.isInteger(slot) || slot < 1 || slot > baseConfig.cwdChoices.length) throw new Error(`Usage: /cwd <1-${baseConfig.cwdChoices.length}>`);
|
|
14924
15308
|
const target = baseConfig.cwdChoices[slot - 1];
|
|
14925
|
-
if (!
|
|
15309
|
+
if (!isAbsolute7(target) || !(await stat9(target)).isDirectory()) throw new Error(`Configured CWD ${slot} must be an existing absolute directory`);
|
|
14926
15310
|
this.saveChatConfig(clientId, { defaultCwd: target });
|
|
14927
15311
|
this.resetSession(clientId);
|
|
14928
|
-
await this.sendReply(baseConfig,
|
|
15312
|
+
await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Working directory updated."));
|
|
14929
15313
|
return true;
|
|
14930
15314
|
}
|
|
14931
15315
|
if (normalized === "/skillsets") {
|
|
14932
|
-
await this.sendReply(baseConfig,
|
|
15316
|
+
await this.sendReply(baseConfig, message2.userId, this.formatSkillsets(effectiveConfig.skillPresetId));
|
|
14933
15317
|
return true;
|
|
14934
15318
|
}
|
|
14935
15319
|
if (normalized === "/skillset") {
|
|
@@ -14937,22 +15321,22 @@ var WecomGatewayService = class {
|
|
|
14937
15321
|
if (!selected) throw new Error("Usage: /skillset <all|preset-name>");
|
|
14938
15322
|
this.saveChatConfig(clientId, this.resolveSkillsetConfig(selected, true));
|
|
14939
15323
|
this.resetSession(clientId);
|
|
14940
|
-
await this.sendReply(baseConfig,
|
|
15324
|
+
await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Skillset updated."));
|
|
14941
15325
|
return true;
|
|
14942
15326
|
}
|
|
14943
15327
|
if (normalized === "/clear-config") {
|
|
14944
15328
|
this.db.prepare("DELETE FROM wecom_gateway_configs WHERE client_id = ?").run(clientId);
|
|
14945
15329
|
this.resetSession(clientId);
|
|
14946
|
-
await this.sendReply(baseConfig,
|
|
15330
|
+
await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "WeCom chat config cleared."));
|
|
14947
15331
|
return true;
|
|
14948
15332
|
}
|
|
14949
15333
|
} catch (error) {
|
|
14950
|
-
await this.sendReply(baseConfig,
|
|
15334
|
+
await this.sendReply(baseConfig, message2.userId, `Command failed: ${error instanceof Error ? error.message : "unknown error"}
|
|
14951
15335
|
|
|
14952
15336
|
${this.formatHelp()}`);
|
|
14953
15337
|
return true;
|
|
14954
15338
|
}
|
|
14955
|
-
await this.sendReply(baseConfig,
|
|
15339
|
+
await this.sendReply(baseConfig, message2.userId, `Unknown command: ${command}
|
|
14956
15340
|
|
|
14957
15341
|
${this.formatHelp()}`);
|
|
14958
15342
|
return true;
|
|
@@ -15181,8 +15565,8 @@ function normalizeList(value) {
|
|
|
15181
15565
|
function stringValue3(value) {
|
|
15182
15566
|
return typeof value === "string" ? value : value === void 0 || value === null ? "" : String(value);
|
|
15183
15567
|
}
|
|
15184
|
-
function gatewayError(
|
|
15185
|
-
const error = new Error(
|
|
15568
|
+
function gatewayError(message2, statusCode) {
|
|
15569
|
+
const error = new Error(message2);
|
|
15186
15570
|
error.statusCode = statusCode;
|
|
15187
15571
|
return error;
|
|
15188
15572
|
}
|
|
@@ -15198,11 +15582,11 @@ async function parseWecomResponse(response) {
|
|
|
15198
15582
|
}
|
|
15199
15583
|
function extractLastAssistantText3(messages) {
|
|
15200
15584
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
15201
|
-
const
|
|
15202
|
-
if (
|
|
15203
|
-
if (typeof
|
|
15204
|
-
if (!Array.isArray(
|
|
15205
|
-
const text =
|
|
15585
|
+
const message2 = messages[index];
|
|
15586
|
+
if (message2?.role !== "assistant") continue;
|
|
15587
|
+
if (typeof message2.content === "string" && message2.content.trim()) return message2.content.trim();
|
|
15588
|
+
if (!Array.isArray(message2.content)) continue;
|
|
15589
|
+
const text = message2.content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("").trim();
|
|
15206
15590
|
if (text) return text;
|
|
15207
15591
|
}
|
|
15208
15592
|
return void 0;
|
|
@@ -15255,7 +15639,7 @@ var ClaudeCodeReviewSourceAdapter = class {
|
|
|
15255
15639
|
if (!normalized) return this.list(options);
|
|
15256
15640
|
const matches = this.readSessions().filter(({ item, filePath }) => {
|
|
15257
15641
|
if (item.name?.toLowerCase().includes(normalized) || item.firstMessage?.toLowerCase().includes(normalized)) return true;
|
|
15258
|
-
return this.readTranscript(filePath).messages.some((
|
|
15642
|
+
return this.readTranscript(filePath).messages.some((message2) => this.messageText(message2.content).toLowerCase().includes(normalized));
|
|
15259
15643
|
}).map((session) => session.item).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
15260
15644
|
return this.applyListOptions(matches, options);
|
|
15261
15645
|
}
|
|
@@ -15276,14 +15660,14 @@ var ClaudeCodeReviewSourceAdapter = class {
|
|
|
15276
15660
|
try {
|
|
15277
15661
|
const records = this.readRecords(filePath);
|
|
15278
15662
|
if (!records.length) return [];
|
|
15279
|
-
const
|
|
15663
|
+
const stat10 = fs5.statSync(filePath);
|
|
15280
15664
|
const sessionRecord = records.find((record) => record.sessionId || record.session_id);
|
|
15281
15665
|
const sessionId = sessionRecord?.sessionId || sessionRecord?.session_id || path7.basename(filePath, ".jsonl");
|
|
15282
15666
|
const cwd = records.find((record) => record.cwd)?.cwd;
|
|
15283
15667
|
const timestamps = records.map((record) => this.timestamp(record.timestamp)).filter((value) => value !== void 0);
|
|
15284
15668
|
const messages = this.recordsToMessages(records);
|
|
15285
15669
|
const title = [...records].reverse().find((record) => typeof record.aiTitle === "string")?.aiTitle;
|
|
15286
|
-
const firstUserMessage = messages.find((
|
|
15670
|
+
const firstUserMessage = messages.find((message2) => message2.role === "user");
|
|
15287
15671
|
return [{
|
|
15288
15672
|
filePath,
|
|
15289
15673
|
item: {
|
|
@@ -15292,8 +15676,8 @@ var ClaudeCodeReviewSourceAdapter = class {
|
|
|
15292
15676
|
name: title,
|
|
15293
15677
|
path: filePath,
|
|
15294
15678
|
cwd,
|
|
15295
|
-
created: new Date(timestamps.length ? Math.min(...timestamps) :
|
|
15296
|
-
modified: new Date(timestamps.length ? Math.max(...timestamps) :
|
|
15679
|
+
created: new Date(timestamps.length ? Math.min(...timestamps) : stat10.birthtimeMs).toISOString(),
|
|
15680
|
+
modified: new Date(timestamps.length ? Math.max(...timestamps) : stat10.mtimeMs).toISOString(),
|
|
15297
15681
|
messageCount: messages.length,
|
|
15298
15682
|
firstMessage: firstUserMessage ? this.messageText(firstUserMessage.content) : void 0
|
|
15299
15683
|
}
|
|
@@ -15418,7 +15802,7 @@ var CodexReviewSourceAdapter = class {
|
|
|
15418
15802
|
if (!normalized) return this.list(options);
|
|
15419
15803
|
const matches = this.readSessions().filter(({ item, filePath }) => {
|
|
15420
15804
|
if (item.name?.toLowerCase().includes(normalized) || item.firstMessage?.toLowerCase().includes(normalized)) return true;
|
|
15421
|
-
return this.readTranscript(filePath).messages.some((
|
|
15805
|
+
return this.readTranscript(filePath).messages.some((message2) => this.messageText(message2.content).toLowerCase().includes(normalized));
|
|
15422
15806
|
}).map((session) => session.item).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
15423
15807
|
return this.applyListOptions(matches, options);
|
|
15424
15808
|
}
|
|
@@ -15439,11 +15823,11 @@ var CodexReviewSourceAdapter = class {
|
|
|
15439
15823
|
try {
|
|
15440
15824
|
const records = this.readRecords(filePath);
|
|
15441
15825
|
if (!records.length) return [];
|
|
15442
|
-
const
|
|
15826
|
+
const stat10 = fs6.statSync(filePath);
|
|
15443
15827
|
const metadata = records.find((record) => record.type === "session_meta")?.payload;
|
|
15444
15828
|
const messages = this.recordsToMessages(records);
|
|
15445
15829
|
const timestamps = records.map((record) => this.timestamp(record.timestamp)).filter((value) => value !== void 0);
|
|
15446
|
-
const firstUserMessage = messages.find((
|
|
15830
|
+
const firstUserMessage = messages.find((message2) => message2.role === "user" && !message2.detailOnly);
|
|
15447
15831
|
return [{
|
|
15448
15832
|
filePath,
|
|
15449
15833
|
item: {
|
|
@@ -15451,8 +15835,8 @@ var CodexReviewSourceAdapter = class {
|
|
|
15451
15835
|
sourceId: "",
|
|
15452
15836
|
path: filePath,
|
|
15453
15837
|
cwd: this.stringValue(metadata?.cwd),
|
|
15454
|
-
created: new Date(timestamps.length ? Math.min(...timestamps) :
|
|
15455
|
-
modified: new Date(timestamps.length ? Math.max(...timestamps) :
|
|
15838
|
+
created: new Date(timestamps.length ? Math.min(...timestamps) : stat10.birthtimeMs).toISOString(),
|
|
15839
|
+
modified: new Date(timestamps.length ? Math.max(...timestamps) : stat10.mtimeMs).toISOString(),
|
|
15456
15840
|
messageCount: messages.length,
|
|
15457
15841
|
firstMessage: firstUserMessage ? this.messageText(firstUserMessage.content) : void 0
|
|
15458
15842
|
}
|
|
@@ -15719,15 +16103,15 @@ var DevinReviewSourceAdapter = class {
|
|
|
15719
16103
|
}
|
|
15720
16104
|
extractMessages(raw) {
|
|
15721
16105
|
if (Array.isArray(raw.messages) && raw.messages.length > 0) {
|
|
15722
|
-
return raw.messages.map((
|
|
15723
|
-
role: String(
|
|
15724
|
-
content:
|
|
15725
|
-
timestamp: typeof
|
|
15726
|
-
detailOnly: this.isDetailOnlyMessage(
|
|
16106
|
+
return raw.messages.map((message2) => ({
|
|
16107
|
+
role: String(message2.role || "unknown"),
|
|
16108
|
+
content: message2.content,
|
|
16109
|
+
timestamp: typeof message2.timestamp === "number" ? message2.timestamp : void 0,
|
|
16110
|
+
detailOnly: this.isDetailOnlyMessage(message2.content)
|
|
15727
16111
|
}));
|
|
15728
16112
|
}
|
|
15729
16113
|
const steps = Array.isArray(raw.steps) ? raw.steps : [];
|
|
15730
|
-
return steps.map((step) => this.devinStepToMessage(step)).filter((
|
|
16114
|
+
return steps.map((step) => this.devinStepToMessage(step)).filter((message2) => message2.content !== "");
|
|
15731
16115
|
}
|
|
15732
16116
|
devinStepToMessage(step) {
|
|
15733
16117
|
const source = String(step.source || "unknown");
|
|
@@ -15769,16 +16153,16 @@ ${observation}
|
|
|
15769
16153
|
const text = typeof content === "string" ? content.trimStart() : "";
|
|
15770
16154
|
return text.startsWith("You are Devin, an interactive command line") || text.startsWith("You are powered by ") || /^Available subagent profiles for the\s+`?run_subagent`?\s+tool\./.test(text) || text.startsWith("The following skills can be invoked using the `skill` tool.");
|
|
15771
16155
|
}
|
|
15772
|
-
messageToText(
|
|
15773
|
-
if (!
|
|
15774
|
-
if (typeof
|
|
15775
|
-
if (Array.isArray(
|
|
15776
|
-
return
|
|
16156
|
+
messageToText(message2) {
|
|
16157
|
+
if (!message2) return void 0;
|
|
16158
|
+
if (typeof message2.content === "string") return message2.content;
|
|
16159
|
+
if (Array.isArray(message2.content)) {
|
|
16160
|
+
return message2.content.map((item) => typeof item === "string" ? item : item?.text).filter(Boolean).join("\n");
|
|
15777
16161
|
}
|
|
15778
16162
|
return void 0;
|
|
15779
16163
|
}
|
|
15780
16164
|
transcriptToText(transcript) {
|
|
15781
|
-
return transcript.messages.map((
|
|
16165
|
+
return transcript.messages.map((message2) => this.messageToText(message2) || "").join("\n");
|
|
15782
16166
|
}
|
|
15783
16167
|
applyListOptions(items, options) {
|
|
15784
16168
|
let result = items;
|
|
@@ -15988,6 +16372,336 @@ var ReviewSourceService = class {
|
|
|
15988
16372
|
}
|
|
15989
16373
|
};
|
|
15990
16374
|
|
|
16375
|
+
// src/services/model-window-kickoff-store.ts
|
|
16376
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
16377
|
+
var DEFAULT_KICKOFF_PROMPT = "Ping";
|
|
16378
|
+
var ModelWindowKickoffStore = class {
|
|
16379
|
+
constructor(db) {
|
|
16380
|
+
this.db = db;
|
|
16381
|
+
}
|
|
16382
|
+
db;
|
|
16383
|
+
list() {
|
|
16384
|
+
return this.db.prepare("SELECT * FROM model_window_kickoffs ORDER BY created_at").all().map(toRecord);
|
|
16385
|
+
}
|
|
16386
|
+
get(id) {
|
|
16387
|
+
const row = this.db.prepare("SELECT * FROM model_window_kickoffs WHERE id = ?").get(id);
|
|
16388
|
+
return row && toRecord(row);
|
|
16389
|
+
}
|
|
16390
|
+
listDue(now) {
|
|
16391
|
+
return this.db.prepare(`
|
|
16392
|
+
SELECT * FROM model_window_kickoffs WHERE enabled = 1 AND next_run_at <= ? ORDER BY next_run_at
|
|
16393
|
+
`).all(now).map(toRecord);
|
|
16394
|
+
}
|
|
16395
|
+
create(input, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
16396
|
+
const value = validateInput2(input, now);
|
|
16397
|
+
const id = randomUUID8();
|
|
16398
|
+
this.db.prepare(`
|
|
16399
|
+
INSERT INTO model_window_kickoffs (
|
|
16400
|
+
id, enabled, profile_id, provider, model_id, project_path, prompt, window_duration_minutes,
|
|
16401
|
+
safety_buffer_seconds, notification_channel_id, next_run_at, created_at, updated_at
|
|
16402
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
16403
|
+
`).run(
|
|
16404
|
+
id,
|
|
16405
|
+
value.enabled ? 1 : 0,
|
|
16406
|
+
value.profileId,
|
|
16407
|
+
value.provider,
|
|
16408
|
+
value.modelId,
|
|
16409
|
+
value.projectPath,
|
|
16410
|
+
value.prompt,
|
|
16411
|
+
value.windowDurationMinutes,
|
|
16412
|
+
value.safetyBufferSeconds,
|
|
16413
|
+
value.notificationChannelId,
|
|
16414
|
+
value.nextRunAt,
|
|
16415
|
+
now,
|
|
16416
|
+
now
|
|
16417
|
+
);
|
|
16418
|
+
return this.get(id);
|
|
16419
|
+
}
|
|
16420
|
+
update(id, input, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
16421
|
+
if (!this.get(id)) throw new Error("Model window kickoff not found");
|
|
16422
|
+
const value = validateInput2(input, now);
|
|
16423
|
+
this.db.prepare(`
|
|
16424
|
+
UPDATE model_window_kickoffs SET enabled = ?, profile_id = ?, provider = ?, model_id = ?, project_path = ?, prompt = ?,
|
|
16425
|
+
window_duration_minutes = ?, safety_buffer_seconds = ?, notification_channel_id = ?, next_run_at = ?, updated_at = ?
|
|
16426
|
+
WHERE id = ?
|
|
16427
|
+
`).run(
|
|
16428
|
+
value.enabled ? 1 : 0,
|
|
16429
|
+
value.profileId,
|
|
16430
|
+
value.provider,
|
|
16431
|
+
value.modelId,
|
|
16432
|
+
value.projectPath,
|
|
16433
|
+
value.prompt,
|
|
16434
|
+
value.windowDurationMinutes,
|
|
16435
|
+
value.safetyBufferSeconds,
|
|
16436
|
+
value.notificationChannelId,
|
|
16437
|
+
value.nextRunAt,
|
|
16438
|
+
now,
|
|
16439
|
+
id
|
|
16440
|
+
);
|
|
16441
|
+
return this.get(id);
|
|
16442
|
+
}
|
|
16443
|
+
delete(id) {
|
|
16444
|
+
if (this.db.prepare("DELETE FROM model_window_kickoffs WHERE id = ?").run(id).changes !== 1) {
|
|
16445
|
+
throw new Error("Model window kickoff not found");
|
|
16446
|
+
}
|
|
16447
|
+
}
|
|
16448
|
+
recordAttempt(id, at) {
|
|
16449
|
+
this.db.prepare("UPDATE model_window_kickoffs SET last_attempt_at = ?, updated_at = ? WHERE id = ?").run(at, at, id);
|
|
16450
|
+
}
|
|
16451
|
+
recordSuccess(id, at, nextRunAt) {
|
|
16452
|
+
this.db.prepare(`UPDATE model_window_kickoffs SET last_success_at = ?, next_run_at = ?, last_error = NULL, updated_at = ? WHERE id = ?`).run(at, nextRunAt, at, id);
|
|
16453
|
+
}
|
|
16454
|
+
recordFailure(id, at, error, nextRunAt) {
|
|
16455
|
+
this.db.prepare(`UPDATE model_window_kickoffs SET last_error = ?, next_run_at = ?, updated_at = ? WHERE id = ?`).run(error.slice(0, 2e3), nextRunAt, at, id);
|
|
16456
|
+
}
|
|
16457
|
+
};
|
|
16458
|
+
function validateInput2(input, now) {
|
|
16459
|
+
const profileId = input.profileId?.trim();
|
|
16460
|
+
const provider = input.provider?.trim();
|
|
16461
|
+
const modelId = input.modelId?.trim();
|
|
16462
|
+
const projectPath = input.projectPath?.trim() || "~";
|
|
16463
|
+
const prompt = (input.prompt ?? DEFAULT_KICKOFF_PROMPT).trim();
|
|
16464
|
+
const windowDurationMinutes = input.windowDurationMinutes ?? 300;
|
|
16465
|
+
const safetyBufferSeconds = input.safetyBufferSeconds ?? 60;
|
|
16466
|
+
const nextRunAt = input.nextRunAt ?? now;
|
|
16467
|
+
if (!profileId || !provider || !modelId || !prompt) {
|
|
16468
|
+
throw new Error("Profile, provider, model, and prompt are required");
|
|
16469
|
+
}
|
|
16470
|
+
if (!Number.isInteger(windowDurationMinutes) || windowDurationMinutes < 1 || windowDurationMinutes > 43200) {
|
|
16471
|
+
throw new Error("Window duration must be between 1 and 43200 minutes");
|
|
16472
|
+
}
|
|
16473
|
+
if (!Number.isInteger(safetyBufferSeconds) || safetyBufferSeconds < 0 || safetyBufferSeconds > 86400) {
|
|
16474
|
+
throw new Error("Safety buffer must be between 0 and 86400 seconds");
|
|
16475
|
+
}
|
|
16476
|
+
if (!Number.isFinite(Date.parse(nextRunAt))) {
|
|
16477
|
+
throw new Error("Next run time is invalid");
|
|
16478
|
+
}
|
|
16479
|
+
return {
|
|
16480
|
+
enabled: input.enabled ?? true,
|
|
16481
|
+
profileId,
|
|
16482
|
+
provider,
|
|
16483
|
+
modelId,
|
|
16484
|
+
projectPath,
|
|
16485
|
+
prompt,
|
|
16486
|
+
windowDurationMinutes,
|
|
16487
|
+
safetyBufferSeconds,
|
|
16488
|
+
notificationChannelId: input.notificationChannelId || null,
|
|
16489
|
+
nextRunAt: new Date(nextRunAt).toISOString()
|
|
16490
|
+
};
|
|
16491
|
+
}
|
|
16492
|
+
function toRecord(row) {
|
|
16493
|
+
return {
|
|
16494
|
+
id: row.id,
|
|
16495
|
+
enabled: Boolean(row.enabled),
|
|
16496
|
+
profileId: row.profile_id,
|
|
16497
|
+
provider: row.provider,
|
|
16498
|
+
modelId: row.model_id,
|
|
16499
|
+
projectPath: row.project_path,
|
|
16500
|
+
prompt: row.prompt,
|
|
16501
|
+
windowDurationMinutes: row.window_duration_minutes,
|
|
16502
|
+
safetyBufferSeconds: row.safety_buffer_seconds,
|
|
16503
|
+
notificationChannelId: row.notification_channel_id,
|
|
16504
|
+
nextRunAt: row.next_run_at,
|
|
16505
|
+
lastAttemptAt: row.last_attempt_at,
|
|
16506
|
+
lastSuccessAt: row.last_success_at,
|
|
16507
|
+
lastError: row.last_error,
|
|
16508
|
+
createdAt: row.created_at,
|
|
16509
|
+
updatedAt: row.updated_at
|
|
16510
|
+
};
|
|
16511
|
+
}
|
|
16512
|
+
|
|
16513
|
+
// src/services/model-window-kickoff-scheduler.ts
|
|
16514
|
+
var POLL_INTERVAL_MS = 1 * 6e4;
|
|
16515
|
+
var FAILURE_RETRY_MS = 60 * 6e4;
|
|
16516
|
+
var ModelWindowKickoffScheduler = class {
|
|
16517
|
+
constructor(dependencies) {
|
|
16518
|
+
this.dependencies = dependencies;
|
|
16519
|
+
this.now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
|
|
16520
|
+
}
|
|
16521
|
+
dependencies;
|
|
16522
|
+
timer;
|
|
16523
|
+
running = /* @__PURE__ */ new Set();
|
|
16524
|
+
now;
|
|
16525
|
+
start() {
|
|
16526
|
+
if (this.timer) return;
|
|
16527
|
+
void this.tick();
|
|
16528
|
+
this.timer = setInterval(() => void this.tick(), this.dependencies.pollIntervalMs ?? POLL_INTERVAL_MS);
|
|
16529
|
+
this.timer.unref();
|
|
16530
|
+
}
|
|
16531
|
+
async stop() {
|
|
16532
|
+
if (this.timer) clearInterval(this.timer);
|
|
16533
|
+
this.timer = void 0;
|
|
16534
|
+
while (this.running.size) await new Promise((resolve10) => setTimeout(resolve10, 10));
|
|
16535
|
+
}
|
|
16536
|
+
async tick() {
|
|
16537
|
+
const due = this.dependencies.store.listDue(this.now().toISOString());
|
|
16538
|
+
await Promise.allSettled(due.map((kickoff) => this.run(kickoff)));
|
|
16539
|
+
}
|
|
16540
|
+
async run(kickoff) {
|
|
16541
|
+
if (this.running.has(kickoff.id)) return;
|
|
16542
|
+
this.running.add(kickoff.id);
|
|
16543
|
+
const attemptedAt = this.now();
|
|
16544
|
+
this.dependencies.store.recordAttempt(kickoff.id, attemptedAt.toISOString());
|
|
16545
|
+
try {
|
|
16546
|
+
const profile = await this.dependencies.resolveProfile(kickoff.profileId);
|
|
16547
|
+
if (!profile) throw new Error("Agent profile not found");
|
|
16548
|
+
const response = this.dependencies.probe ? await this.dependencies.probe(kickoff, profile) : await probeModel(kickoff, this.dependencies.sessions);
|
|
16549
|
+
const successfulAt = this.now();
|
|
16550
|
+
const next = new Date(successfulAt.getTime() + kickoff.windowDurationMinutes * 6e4 + kickoff.safetyBufferSeconds * 1e3);
|
|
16551
|
+
this.dependencies.store.recordSuccess(kickoff.id, successfulAt.toISOString(), next.toISOString());
|
|
16552
|
+
if (kickoff.notificationChannelId) {
|
|
16553
|
+
try {
|
|
16554
|
+
await this.dependencies.notifications.send(
|
|
16555
|
+
kickoff.notificationChannelId,
|
|
16556
|
+
formatSuccessNotification(kickoff, profile, response, attemptedAt, next)
|
|
16557
|
+
);
|
|
16558
|
+
} catch (error) {
|
|
16559
|
+
this.dependencies.log?.error(error, "Model window kickoff notification failed");
|
|
16560
|
+
}
|
|
16561
|
+
}
|
|
16562
|
+
} catch (error) {
|
|
16563
|
+
const failedAt = this.now();
|
|
16564
|
+
const failure = error instanceof Error ? error.message : String(error);
|
|
16565
|
+
const retryAt = new Date(failedAt.getTime() + FAILURE_RETRY_MS);
|
|
16566
|
+
this.dependencies.store.recordFailure(kickoff.id, failedAt.toISOString(), failure, retryAt.toISOString());
|
|
16567
|
+
if (kickoff.notificationChannelId) {
|
|
16568
|
+
try {
|
|
16569
|
+
await this.dependencies.notifications.send(
|
|
16570
|
+
kickoff.notificationChannelId,
|
|
16571
|
+
formatFailureNotification(kickoff, failure, attemptedAt, retryAt)
|
|
16572
|
+
);
|
|
16573
|
+
} catch (notificationError) {
|
|
16574
|
+
this.dependencies.log?.error(notificationError, "Model window kickoff failure notification failed");
|
|
16575
|
+
}
|
|
16576
|
+
}
|
|
16577
|
+
} finally {
|
|
16578
|
+
this.running.delete(kickoff.id);
|
|
16579
|
+
}
|
|
16580
|
+
}
|
|
16581
|
+
};
|
|
16582
|
+
function formatSuccessNotification(kickoff, profile, response, startedAt, next) {
|
|
16583
|
+
return [
|
|
16584
|
+
"\u2705Model window started",
|
|
16585
|
+
markdownField("Profile", profile.label || profile.id),
|
|
16586
|
+
markdownField("Model", `${kickoff.provider}/${kickoff.modelId}`),
|
|
16587
|
+
...response ? [markdownField("Response", response)] : [],
|
|
16588
|
+
markdownField("Started time", formatLocalDateTime(startedAt), "warning"),
|
|
16589
|
+
markdownField("Next kickoff", formatLocalDateTime(next))
|
|
16590
|
+
].join("\n");
|
|
16591
|
+
}
|
|
16592
|
+
function formatFailureNotification(kickoff, failure, startedAt, retryAt) {
|
|
16593
|
+
return [
|
|
16594
|
+
"\u274CModel window kickoff failed",
|
|
16595
|
+
markdownField("Profile", kickoff.profileId),
|
|
16596
|
+
markdownField("Model", `${kickoff.provider}/${kickoff.modelId}`),
|
|
16597
|
+
markdownField("Error", failure, "warning"),
|
|
16598
|
+
markdownField("Started time", formatLocalDateTime(startedAt), "warning"),
|
|
16599
|
+
markdownField("Retry", formatLocalDateTime(retryAt))
|
|
16600
|
+
].join("\n");
|
|
16601
|
+
}
|
|
16602
|
+
function markdownField(label, value, color = "comment") {
|
|
16603
|
+
return `>${label}: <font color="${color}">${value}</font>`;
|
|
16604
|
+
}
|
|
16605
|
+
function formatLocalDateTime(value) {
|
|
16606
|
+
const pad = (part) => String(part).padStart(2, "0");
|
|
16607
|
+
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`;
|
|
16608
|
+
}
|
|
16609
|
+
async function probeModel(kickoff, sessions) {
|
|
16610
|
+
const clientId = `model-window-kickoff:${kickoff.id}`;
|
|
16611
|
+
const sessionName = `Model window kickoff: ${kickoff.provider}/${kickoff.modelId} (${kickoff.id.slice(0, 8)})`;
|
|
16612
|
+
await sessions.setClientAgentProfile(clientId, kickoff.profileId);
|
|
16613
|
+
const persisted = (await sessions.listSessions(clientId, kickoff.projectPath)).find((item) => item.name === sessionName);
|
|
16614
|
+
let activeSession;
|
|
16615
|
+
if (persisted) {
|
|
16616
|
+
activeSession = await sessions.resumeSession(clientId, persisted.path);
|
|
16617
|
+
} else {
|
|
16618
|
+
activeSession = (await sessions.createSession(clientId, {
|
|
16619
|
+
cwd: kickoff.projectPath,
|
|
16620
|
+
agentProfileId: kickoff.profileId,
|
|
16621
|
+
modelProvider: kickoff.provider,
|
|
16622
|
+
modelId: kickoff.modelId,
|
|
16623
|
+
memoryEnabled: false
|
|
16624
|
+
})).session;
|
|
16625
|
+
await sessions.renameSession(clientId, activeSession.sessionId, sessionName);
|
|
16626
|
+
}
|
|
16627
|
+
await sessions.setSessionModel(clientId, activeSession.sessionId, kickoff.provider, kickoff.modelId);
|
|
16628
|
+
await sessions.runForegroundWithClientProfileProxy(clientId, () => activeSession.prompt(kickoff.prompt));
|
|
16629
|
+
const response = [...activeSession.messages].reverse().find((message2) => message2.role === "assistant" && "stopReason" in message2);
|
|
16630
|
+
if (!response || !("stopReason" in response)) throw new Error("Model kickoff finished without an assistant response");
|
|
16631
|
+
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
16632
|
+
throw new Error(response.errorMessage || `Model kickoff ${response.stopReason}`);
|
|
16633
|
+
}
|
|
16634
|
+
return extractAssistantText(response.content);
|
|
16635
|
+
}
|
|
16636
|
+
function extractAssistantText(content) {
|
|
16637
|
+
if (typeof content === "string") return content.trim();
|
|
16638
|
+
if (!Array.isArray(content)) return "";
|
|
16639
|
+
return content.filter((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string").map((part) => part.text).join("").trim();
|
|
16640
|
+
}
|
|
16641
|
+
|
|
16642
|
+
// src/services/notification-channel.ts
|
|
16643
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
16644
|
+
var NotificationChannelService = class {
|
|
16645
|
+
constructor(db) {
|
|
16646
|
+
this.db = db;
|
|
16647
|
+
}
|
|
16648
|
+
db;
|
|
16649
|
+
list() {
|
|
16650
|
+
return this.db.prepare("SELECT id, type, name, config_json FROM notification_channels ORDER BY created_at").all().map(publicChannel);
|
|
16651
|
+
}
|
|
16652
|
+
saveWecom(input) {
|
|
16653
|
+
const existing = input.id ? this.getRow(input.id) : void 0;
|
|
16654
|
+
const botKey = input.botKey?.trim() || readBotKey(existing);
|
|
16655
|
+
if (!botKey) throw new Error("WeCom bot key is required");
|
|
16656
|
+
const id = existing?.id ?? randomUUID9();
|
|
16657
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16658
|
+
const name = input.name?.trim() || existing?.name || "WeCom";
|
|
16659
|
+
this.db.prepare(`
|
|
16660
|
+
INSERT INTO notification_channels (id, type, name, config_json, created_at, updated_at)
|
|
16661
|
+
VALUES (?, 'wecom', ?, ?, ?, ?)
|
|
16662
|
+
ON CONFLICT(id) DO UPDATE SET name = excluded.name, config_json = excluded.config_json, updated_at = excluded.updated_at
|
|
16663
|
+
`).run(id, name, JSON.stringify({ botKey }), now, now);
|
|
16664
|
+
return publicChannel(this.getRow(id));
|
|
16665
|
+
}
|
|
16666
|
+
delete(id) {
|
|
16667
|
+
if (this.db.prepare("DELETE FROM notification_channels WHERE id = ?").run(id).changes !== 1) {
|
|
16668
|
+
throw new Error("Notification channel not found");
|
|
16669
|
+
}
|
|
16670
|
+
}
|
|
16671
|
+
async send(id, message2) {
|
|
16672
|
+
const row = this.getRow(id);
|
|
16673
|
+
if (!row) throw new Error("Notification channel not found");
|
|
16674
|
+
if (row.type !== "wecom") throw new Error(`Unsupported notification channel: ${row.type}`);
|
|
16675
|
+
const botKey = readBotKey(row);
|
|
16676
|
+
if (!botKey) throw new Error("WeCom bot key is not configured");
|
|
16677
|
+
const response = await fetch(`https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${encodeURIComponent(botKey)}`, {
|
|
16678
|
+
method: "POST",
|
|
16679
|
+
headers: { "content-type": "application/json" },
|
|
16680
|
+
body: JSON.stringify({ msgtype: "markdown", markdown: { content: message2 } }),
|
|
16681
|
+
signal: AbortSignal.timeout(1e4)
|
|
16682
|
+
});
|
|
16683
|
+
if (!response.ok) throw new Error(`WeCom returned HTTP ${response.status}`);
|
|
16684
|
+
const result = await response.json();
|
|
16685
|
+
if (result.errcode !== 0) throw new Error(`WeCom notification failed: ${result.errmsg || result.errcode}`);
|
|
16686
|
+
}
|
|
16687
|
+
getRow(id) {
|
|
16688
|
+
return this.db.prepare("SELECT id, type, name, config_json FROM notification_channels WHERE id = ?").get(id);
|
|
16689
|
+
}
|
|
16690
|
+
};
|
|
16691
|
+
function readBotKey(row) {
|
|
16692
|
+
if (!row) {
|
|
16693
|
+
return "";
|
|
16694
|
+
}
|
|
16695
|
+
try {
|
|
16696
|
+
return JSON.parse(row.config_json).botKey || "";
|
|
16697
|
+
} catch {
|
|
16698
|
+
return "";
|
|
16699
|
+
}
|
|
16700
|
+
}
|
|
16701
|
+
function publicChannel(row) {
|
|
16702
|
+
return { id: row.id, type: "wecom", name: row.name, configured: Boolean(readBotKey(row)) };
|
|
16703
|
+
}
|
|
16704
|
+
|
|
15991
16705
|
// src/index.ts
|
|
15992
16706
|
var __filename = fileURLToPath2(import.meta.url);
|
|
15993
16707
|
var __dirname = path12.dirname(__filename);
|
|
@@ -16076,8 +16790,8 @@ function isSpaRoute(req) {
|
|
|
16076
16790
|
if (path12.extname(url)) return false;
|
|
16077
16791
|
return true;
|
|
16078
16792
|
}
|
|
16079
|
-
function
|
|
16080
|
-
const row = db.prepare("SELECT value FROM
|
|
16793
|
+
function getApplicationSetting(db, key) {
|
|
16794
|
+
const row = db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
|
|
16081
16795
|
return row?.value || "";
|
|
16082
16796
|
}
|
|
16083
16797
|
function formatServerTimestamp() {
|
|
@@ -16135,6 +16849,16 @@ async function buildApp() {
|
|
|
16135
16849
|
const skillPresetStore = new SkillPresetStore(db);
|
|
16136
16850
|
const reviewSourceStore = new ReviewSourceStore(db);
|
|
16137
16851
|
const reviewSourceService = new ReviewSourceService(reviewSourceStore);
|
|
16852
|
+
const modelWindowKickoffStore = new ModelWindowKickoffStore(db);
|
|
16853
|
+
const notificationChannels = new NotificationChannelService(db);
|
|
16854
|
+
const modelWindowKickoffScheduler = new ModelWindowKickoffScheduler({
|
|
16855
|
+
store: modelWindowKickoffStore,
|
|
16856
|
+
notifications: notificationChannels,
|
|
16857
|
+
sessions: piSessionService,
|
|
16858
|
+
resolveProfile: async (profileId) => (await piSessionService.listAgentProfiles()).find((profile) => profile.id === profileId),
|
|
16859
|
+
log: app.log
|
|
16860
|
+
});
|
|
16861
|
+
modelWindowKickoffScheduler.start();
|
|
16138
16862
|
const sessionActivityStore = new SessionActivityStore(db);
|
|
16139
16863
|
const sessionPinStore = new SessionPinStore(db);
|
|
16140
16864
|
projectTaskStore.restoreAllStarting();
|
|
@@ -16153,7 +16877,7 @@ async function buildApp() {
|
|
|
16153
16877
|
...await piSessionService.getClientAgentProxyEnvForRoutes(clientId || "default"),
|
|
16154
16878
|
...githubSettings.proxyEnv()
|
|
16155
16879
|
}),
|
|
16156
|
-
gitCloneParentPath: () =>
|
|
16880
|
+
gitCloneParentPath: () => getApplicationSetting(db, "ui.gitCloneParentPath") || path12.join(os9.homedir(), "git", "github")
|
|
16157
16881
|
});
|
|
16158
16882
|
const gitHosting = new GitHostingService();
|
|
16159
16883
|
const worktreeManager = new WorktreeManager();
|
|
@@ -16178,6 +16902,7 @@ async function buildApp() {
|
|
|
16178
16902
|
worktreeMetadata: worktreeMetadataStore
|
|
16179
16903
|
});
|
|
16180
16904
|
app.addHook("onClose", async () => {
|
|
16905
|
+
await modelWindowKickoffScheduler.stop();
|
|
16181
16906
|
await weixinGateway.stop();
|
|
16182
16907
|
terminalManager.disposeAll();
|
|
16183
16908
|
piSessionService.disposeAll();
|
|
@@ -16266,6 +16991,12 @@ async function buildApp() {
|
|
|
16266
16991
|
await app.register(changelogRoutes, { prefix: "/api/changelog" });
|
|
16267
16992
|
await app.register(speechRoutes, { prefix: "/api/speech" });
|
|
16268
16993
|
await app.register(reviewSourceRoutes, { prefix: "/api/review-sources", reviewSourceService, pinStore: sessionPinStore });
|
|
16994
|
+
await app.register(modelWindowKickoffRoutes, {
|
|
16995
|
+
prefix: "/api/model-window-kickoffs",
|
|
16996
|
+
store: modelWindowKickoffStore,
|
|
16997
|
+
notifications: notificationChannels,
|
|
16998
|
+
sessions: piSessionService
|
|
16999
|
+
});
|
|
16269
17000
|
await app.register(chatWebSocket);
|
|
16270
17001
|
await app.register(terminalWebSocket);
|
|
16271
17002
|
app.get("/api/health", async () => {
|