vibe-coding-master 0.4.30 → 0.4.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -14
- package/dist/backend/adapters/git-adapter.js +22 -0
- package/dist/backend/api/gateway-routes.js +3 -0
- package/dist/backend/api/harness-routes.js +7 -0
- package/dist/backend/gateway/channels/lark-channel.js +254 -0
- package/dist/backend/gateway/channels/weixin-ilink-channel.js +10 -3
- package/dist/backend/gateway/gateway-channel.js +16 -0
- package/dist/backend/gateway/gateway-service.js +164 -39
- package/dist/backend/gateway/gateway-settings-service.js +60 -14
- package/dist/backend/server.js +12 -2
- package/dist/backend/services/harness-service.js +32 -0
- package/dist-frontend/assets/index-CKX3RbCz.js +96 -0
- package/dist-frontend/assets/index-CuNHDIFw.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/gateway-design.md +48 -32
- package/docs/product-design.md +20 -15
- package/package.json +2 -1
- package/dist-frontend/assets/index-BV_K-1WH.css +0 -32
- package/dist-frontend/assets/index-DNh_JE6R.js +0 -96
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomInt } from "node:crypto";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { CORE_VCM_ROLE_DEFINITIONS, GATE_REVIEWER_ROLE_DEFINITION, VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
3
4
|
import { VcmError } from "../errors.js";
|
|
@@ -5,8 +6,8 @@ import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
|
5
6
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
6
7
|
import { parseAssistantContent, resolveExistingClaudeTranscriptPath } from "../services/claude-transcript-service.js";
|
|
7
8
|
import { parseGatewayCommand } from "./gateway-command-parser.js";
|
|
8
|
-
const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
9
9
|
const QR_LOGIN_TTL_MS = 8 * 60 * 1000;
|
|
10
|
+
const PAIRING_CODE_TTL_MS = 10 * 60 * 1000;
|
|
10
11
|
const CLOSE_CONFIRM_TTL_MS = 10 * 60 * 1000;
|
|
11
12
|
const POLL_ERROR_BACKOFF_MS = 2_000;
|
|
12
13
|
const POLL_LONG_BACKOFF_MS = 30_000;
|
|
@@ -41,7 +42,7 @@ export function createGatewayService(deps) {
|
|
|
41
42
|
}
|
|
42
43
|
pollStartingPromise = (async () => {
|
|
43
44
|
const settings = await deps.settings.loadSettings();
|
|
44
|
-
if (!settings
|
|
45
|
+
if (!toAccount(settings) || isRunning()) {
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
48
|
const controller = new AbortController();
|
|
@@ -72,14 +73,24 @@ export function createGatewayService(deps) {
|
|
|
72
73
|
pollAbort = null;
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
function resolveChannel(settings) {
|
|
77
|
+
return deps.channels.get(settings.channel);
|
|
78
|
+
}
|
|
75
79
|
function toAccount(settings) {
|
|
76
|
-
|
|
80
|
+
const channel = resolveChannel(settings);
|
|
81
|
+
if (settings.channel === "weixin-ilink" && !settings.binding.token) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
if (settings.channel === "lark" && (!settings.binding.appId || !settings.binding.appSecret)) {
|
|
77
85
|
return undefined;
|
|
78
86
|
}
|
|
79
87
|
return {
|
|
80
88
|
accountId: settings.binding.accountId,
|
|
81
|
-
baseUrl: settings.binding.baseUrl ||
|
|
82
|
-
token: settings.binding.token
|
|
89
|
+
baseUrl: settings.binding.baseUrl || channel.defaultBaseUrl,
|
|
90
|
+
token: settings.binding.token,
|
|
91
|
+
appId: settings.binding.appId,
|
|
92
|
+
appSecret: settings.binding.appSecret,
|
|
93
|
+
homeChatId: settings.binding.homeChatId
|
|
83
94
|
};
|
|
84
95
|
}
|
|
85
96
|
async function pollLoop(signal) {
|
|
@@ -93,8 +104,9 @@ export function createGatewayService(deps) {
|
|
|
93
104
|
await savePollStatus("idle");
|
|
94
105
|
return;
|
|
95
106
|
}
|
|
107
|
+
const channel = resolveChannel(settings);
|
|
96
108
|
try {
|
|
97
|
-
const result = await
|
|
109
|
+
const result = await channel.getUpdates({
|
|
98
110
|
account,
|
|
99
111
|
cursor: settings.binding.getUpdatesBuf,
|
|
100
112
|
timeoutMs,
|
|
@@ -128,7 +140,7 @@ export function createGatewayService(deps) {
|
|
|
128
140
|
}
|
|
129
141
|
consecutiveFailures += 1;
|
|
130
142
|
const message = errorMessage(error);
|
|
131
|
-
const expired = message.toLowerCase().includes("expired");
|
|
143
|
+
const expired = channel.isSessionExpiredError?.(error) ?? message.toLowerCase().includes("expired");
|
|
132
144
|
const settingsAfterError = await deps.settings.loadSettings();
|
|
133
145
|
await deps.settings.saveSettings({
|
|
134
146
|
...settingsAfterError,
|
|
@@ -186,25 +198,38 @@ export function createGatewayService(deps) {
|
|
|
186
198
|
});
|
|
187
199
|
return;
|
|
188
200
|
}
|
|
189
|
-
settings
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
if (settings.channel === "lark" && !settings.binding.boundUserId) {
|
|
202
|
+
settings = await saveInboundMetadata(settings, update);
|
|
203
|
+
const pairing = parseLarkPairingCommand(update.text);
|
|
204
|
+
if (!pairing || !isValidPairingCode(settings, pairing)) {
|
|
205
|
+
await reply(settings, update.fromUserId, "Lark Gateway is not paired. Generate a pairing code in desktop VCM, then send /bind CODE here.");
|
|
206
|
+
await recordMessageStatus("inbound", "ignored", update.text, "lark gateway not paired");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
await deps.settings.saveSettings({
|
|
210
|
+
...settings,
|
|
211
|
+
binding: {
|
|
212
|
+
...settings.binding,
|
|
213
|
+
boundUserId: update.fromUserId,
|
|
214
|
+
loginUserId: update.fromUserId,
|
|
215
|
+
pairingCode: null,
|
|
216
|
+
pairingCodeExpiresAt: null,
|
|
217
|
+
chatIds: update.chatId
|
|
218
|
+
? {
|
|
219
|
+
...settings.binding.chatIds,
|
|
220
|
+
[update.fromUserId]: update.chatId
|
|
221
|
+
}
|
|
222
|
+
: settings.binding.chatIds
|
|
223
|
+
},
|
|
224
|
+
updatedAt: now()
|
|
225
|
+
});
|
|
226
|
+
await reply(await deps.settings.loadSettings(), update.fromUserId, "Lark Gateway bound. Send /help for available commands.");
|
|
227
|
+
await recordMessageStatus("inbound", "ok", update.text, undefined, "bind");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
settings = await saveInboundMetadata(settings, update, { bindIfMissing: true });
|
|
206
231
|
if (settings.binding.boundUserId !== update.fromUserId) {
|
|
207
|
-
await reply(settings, update.fromUserId, "This VCM gateway is already bound to another
|
|
232
|
+
await reply(settings, update.fromUserId, "This VCM gateway is already bound to another user.");
|
|
208
233
|
await recordMessageStatus("inbound", "ignored", update.text, "unbound user");
|
|
209
234
|
return;
|
|
210
235
|
}
|
|
@@ -237,6 +262,31 @@ export function createGatewayService(deps) {
|
|
|
237
262
|
});
|
|
238
263
|
}
|
|
239
264
|
}
|
|
265
|
+
async function saveInboundMetadata(settings, update, options = {}) {
|
|
266
|
+
return deps.settings.saveSettings({
|
|
267
|
+
...settings,
|
|
268
|
+
binding: {
|
|
269
|
+
...settings.binding,
|
|
270
|
+
boundUserId: options.bindIfMissing ? settings.binding.boundUserId ?? update.fromUserId : settings.binding.boundUserId,
|
|
271
|
+
contextTokens: update.contextToken
|
|
272
|
+
? {
|
|
273
|
+
...settings.binding.contextTokens,
|
|
274
|
+
[update.fromUserId]: update.contextToken
|
|
275
|
+
}
|
|
276
|
+
: settings.binding.contextTokens,
|
|
277
|
+
chatIds: update.chatId
|
|
278
|
+
? {
|
|
279
|
+
...settings.binding.chatIds,
|
|
280
|
+
[update.fromUserId]: update.chatId
|
|
281
|
+
}
|
|
282
|
+
: settings.binding.chatIds
|
|
283
|
+
},
|
|
284
|
+
dedupe: {
|
|
285
|
+
recentInboundMessageIds: [...settings.dedupe.recentInboundMessageIds, update.messageId].slice(-1000)
|
|
286
|
+
},
|
|
287
|
+
updatedAt: now()
|
|
288
|
+
});
|
|
289
|
+
}
|
|
240
290
|
async function executeCommand(command, settings) {
|
|
241
291
|
if (!settings.enabled && !COMMANDS_ALLOWED_WHEN_DISABLED.has(command.kind)) {
|
|
242
292
|
return [
|
|
@@ -581,8 +631,10 @@ export function createGatewayService(deps) {
|
|
|
581
631
|
}
|
|
582
632
|
async function startGateway() {
|
|
583
633
|
const settings = await deps.settings.loadSettings();
|
|
584
|
-
if (!settings
|
|
585
|
-
return
|
|
634
|
+
if (!toAccount(settings)) {
|
|
635
|
+
return settings.channel === "lark"
|
|
636
|
+
? "Gateway is not configured. Save Lark App ID and App Secret in desktop VCM first."
|
|
637
|
+
: "Gateway is not bound. Start QR login from desktop VCM first.";
|
|
586
638
|
}
|
|
587
639
|
if (settings.enabled) {
|
|
588
640
|
return "Gateway is already on.";
|
|
@@ -647,9 +699,11 @@ export function createGatewayService(deps) {
|
|
|
647
699
|
return;
|
|
648
700
|
}
|
|
649
701
|
const contextToken = settings.binding.contextTokens[userId];
|
|
650
|
-
|
|
702
|
+
const chatId = settings.binding.chatIds[userId] ?? settings.binding.homeChatId ?? undefined;
|
|
703
|
+
await resolveChannel(settings).sendText({
|
|
651
704
|
account,
|
|
652
705
|
toUserId: userId,
|
|
706
|
+
chatId,
|
|
653
707
|
contextToken,
|
|
654
708
|
text
|
|
655
709
|
});
|
|
@@ -682,6 +736,15 @@ export function createGatewayService(deps) {
|
|
|
682
736
|
`Last poll: ${synced.lastPollStatus.state}${synced.lastPollStatus.error ? ` (${synced.lastPollStatus.error})` : ""}`
|
|
683
737
|
].join("\n");
|
|
684
738
|
}
|
|
739
|
+
function parseLarkPairingCommand(text) {
|
|
740
|
+
const match = text.trim().match(/^\/bind\s+([A-Z0-9]{6,12})$/i);
|
|
741
|
+
return match?.[1]?.toUpperCase() ?? null;
|
|
742
|
+
}
|
|
743
|
+
function isValidPairingCode(settings, code) {
|
|
744
|
+
const expected = settings.binding.pairingCode?.toUpperCase();
|
|
745
|
+
const expiresAt = settings.binding.pairingCodeExpiresAt;
|
|
746
|
+
return Boolean(expected && expected === code.toUpperCase() && expiresAt && Date.parse(expiresAt) > Date.now());
|
|
747
|
+
}
|
|
685
748
|
return {
|
|
686
749
|
async start() {
|
|
687
750
|
await ensurePolling();
|
|
@@ -699,7 +762,7 @@ export function createGatewayService(deps) {
|
|
|
699
762
|
if (settings.enabled) {
|
|
700
763
|
settings = await syncDesktopContext(settings);
|
|
701
764
|
}
|
|
702
|
-
if (settings
|
|
765
|
+
if (toAccount(settings)) {
|
|
703
766
|
await ensurePolling();
|
|
704
767
|
}
|
|
705
768
|
else {
|
|
@@ -713,15 +776,58 @@ export function createGatewayService(deps) {
|
|
|
713
776
|
const settings = await deps.settings.resetBinding();
|
|
714
777
|
return deps.settings.expose(settings, isRunning());
|
|
715
778
|
},
|
|
779
|
+
async createPairingCode() {
|
|
780
|
+
const settings = await deps.settings.loadSettings();
|
|
781
|
+
if (settings.channel !== "lark") {
|
|
782
|
+
throw new VcmError({
|
|
783
|
+
code: "GATEWAY_PAIRING_UNSUPPORTED",
|
|
784
|
+
message: "Pairing codes are only available for Lark Gateway.",
|
|
785
|
+
statusCode: 409
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
if (!settings.binding.appId || !settings.binding.appSecret) {
|
|
789
|
+
throw new VcmError({
|
|
790
|
+
code: "GATEWAY_LARK_CONFIG_MISSING",
|
|
791
|
+
message: "Save Lark App ID and App Secret before creating a pairing code.",
|
|
792
|
+
statusCode: 409
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
const code = randomPairingCode();
|
|
796
|
+
const expiresAt = new Date(Date.now() + PAIRING_CODE_TTL_MS).toISOString();
|
|
797
|
+
const nextSettings = await deps.settings.saveSettings({
|
|
798
|
+
...settings,
|
|
799
|
+
binding: {
|
|
800
|
+
...settings.binding,
|
|
801
|
+
pairingCode: code,
|
|
802
|
+
pairingCodeExpiresAt: expiresAt
|
|
803
|
+
},
|
|
804
|
+
updatedAt: now()
|
|
805
|
+
});
|
|
806
|
+
await ensurePolling();
|
|
807
|
+
return {
|
|
808
|
+
code,
|
|
809
|
+
expiresAt,
|
|
810
|
+
status: deps.settings.expose(nextSettings, isRunning())
|
|
811
|
+
};
|
|
812
|
+
},
|
|
716
813
|
async startQrLogin() {
|
|
717
814
|
const settings = await deps.settings.loadSettings();
|
|
718
|
-
const
|
|
815
|
+
const channel = resolveChannel(settings);
|
|
816
|
+
if (!channel.startQrLogin) {
|
|
817
|
+
throw new VcmError({
|
|
818
|
+
code: "GATEWAY_QR_UNSUPPORTED",
|
|
819
|
+
message: `${channel.label} Gateway does not support QR login.`,
|
|
820
|
+
statusCode: 409
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
const login = await channel.startQrLogin({
|
|
719
824
|
localTokenList: settings.binding.token ? [settings.binding.token] : []
|
|
720
825
|
});
|
|
721
826
|
qrLogin = {
|
|
827
|
+
channel: settings.channel,
|
|
722
828
|
qrcode: login.qrcode,
|
|
723
829
|
qrcodeUrl: login.qrcodeUrl,
|
|
724
|
-
baseUrl: settings.binding.baseUrl ||
|
|
830
|
+
baseUrl: settings.binding.baseUrl || channel.defaultBaseUrl,
|
|
725
831
|
expiresAt: new Date(Date.now() + QR_LOGIN_TTL_MS).toISOString()
|
|
726
832
|
};
|
|
727
833
|
return {
|
|
@@ -738,7 +844,15 @@ export function createGatewayService(deps) {
|
|
|
738
844
|
message: "QR login expired. Start a new QR login."
|
|
739
845
|
};
|
|
740
846
|
}
|
|
741
|
-
const
|
|
847
|
+
const channel = deps.channels.get(qrLogin.channel);
|
|
848
|
+
if (!channel.checkQrLogin) {
|
|
849
|
+
throw new VcmError({
|
|
850
|
+
code: "GATEWAY_QR_UNSUPPORTED",
|
|
851
|
+
message: `${channel.label} Gateway does not support QR login.`,
|
|
852
|
+
statusCode: 409
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
const result = await channel.checkQrLogin({
|
|
742
856
|
baseUrl: qrLogin.baseUrl,
|
|
743
857
|
qrcode: qrLogin.qrcode,
|
|
744
858
|
verifyCode: input.verifyCode
|
|
@@ -746,21 +860,22 @@ export function createGatewayService(deps) {
|
|
|
746
860
|
if (result.status === "scaned_but_redirect" && result.redirectHost) {
|
|
747
861
|
qrLogin = {
|
|
748
862
|
...qrLogin,
|
|
749
|
-
baseUrl: normalizeBaseUrl(result.redirectHost)
|
|
863
|
+
baseUrl: normalizeBaseUrl(result.redirectHost, channel.defaultBaseUrl)
|
|
750
864
|
};
|
|
751
865
|
}
|
|
752
866
|
if (result.status === "confirmed" || result.status === "binded_redirect") {
|
|
753
867
|
const settings = await deps.settings.loadSettings();
|
|
754
868
|
const token = result.token ?? settings.binding.token;
|
|
869
|
+
const boundUserId = result.boundUserId ?? result.loginUserId;
|
|
755
870
|
if (token) {
|
|
756
871
|
await deps.settings.saveSettings({
|
|
757
872
|
...settings,
|
|
758
873
|
binding: {
|
|
759
874
|
...settings.binding,
|
|
760
875
|
accountId: result.accountId ?? settings.binding.accountId,
|
|
761
|
-
baseUrl: normalizeBaseUrl(result.baseUrl ?? settings.binding.baseUrl),
|
|
876
|
+
baseUrl: normalizeBaseUrl(result.baseUrl ?? settings.binding.baseUrl, channel.defaultBaseUrl),
|
|
762
877
|
loginUserId: result.loginUserId ?? settings.binding.loginUserId,
|
|
763
|
-
boundUserId:
|
|
878
|
+
boundUserId: boundUserId ?? settings.binding.boundUserId,
|
|
764
879
|
token
|
|
765
880
|
},
|
|
766
881
|
updatedAt: now()
|
|
@@ -769,11 +884,12 @@ export function createGatewayService(deps) {
|
|
|
769
884
|
await ensurePolling();
|
|
770
885
|
}
|
|
771
886
|
}
|
|
887
|
+
const boundUserId = result.boundUserId ?? result.loginUserId;
|
|
772
888
|
return {
|
|
773
889
|
status: result.status,
|
|
774
890
|
qrcodeUrl: qrLogin?.qrcodeUrl,
|
|
775
891
|
accountId: result.accountId,
|
|
776
|
-
boundUserId
|
|
892
|
+
boundUserId,
|
|
777
893
|
loginUserId: result.loginUserId
|
|
778
894
|
};
|
|
779
895
|
},
|
|
@@ -812,9 +928,10 @@ export function createGatewayService(deps) {
|
|
|
812
928
|
taskSlug: input.taskSlug,
|
|
813
929
|
sourceText: text
|
|
814
930
|
});
|
|
815
|
-
await
|
|
931
|
+
await resolveChannel(settings).sendText({
|
|
816
932
|
account,
|
|
817
933
|
toUserId: boundUserId,
|
|
934
|
+
chatId: settings.binding.chatIds[boundUserId] ?? settings.binding.homeChatId ?? undefined,
|
|
818
935
|
contextToken: settings.binding.contextTokens[boundUserId],
|
|
819
936
|
text: output.text
|
|
820
937
|
});
|
|
@@ -1095,6 +1212,14 @@ function formatAheadBehind(project) {
|
|
|
1095
1212
|
}
|
|
1096
1213
|
return ahead > 0 ? `ahead ${ahead}` : `behind ${behind}`;
|
|
1097
1214
|
}
|
|
1215
|
+
function randomPairingCode() {
|
|
1216
|
+
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
1217
|
+
let out = "";
|
|
1218
|
+
for (let index = 0; index < 8; index += 1) {
|
|
1219
|
+
out += alphabet[randomInt(alphabet.length)];
|
|
1220
|
+
}
|
|
1221
|
+
return out;
|
|
1222
|
+
}
|
|
1098
1223
|
function sleep(ms, signal) {
|
|
1099
1224
|
return new Promise((resolve) => {
|
|
1100
1225
|
if (signal.aborted) {
|
|
@@ -1108,10 +1233,10 @@ function sleep(ms, signal) {
|
|
|
1108
1233
|
}, { once: true });
|
|
1109
1234
|
});
|
|
1110
1235
|
}
|
|
1111
|
-
function normalizeBaseUrl(input) {
|
|
1236
|
+
function normalizeBaseUrl(input, fallback) {
|
|
1112
1237
|
const trimmed = input.trim();
|
|
1113
1238
|
if (!trimmed) {
|
|
1114
|
-
return
|
|
1239
|
+
return fallback;
|
|
1115
1240
|
}
|
|
1116
1241
|
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
|
|
1117
1242
|
return trimmed.replace(/\/+$/, "");
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { resolveVcmDataDir } from "../vcm-data-dir.js";
|
|
3
3
|
const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
4
|
+
const DEFAULT_GATEWAY_CHANNEL = "weixin-ilink";
|
|
4
5
|
const MAX_DEDUPE_IDS = 1000;
|
|
5
6
|
export function createGatewaySettingsService(deps) {
|
|
6
7
|
const dataDir = resolveVcmDataDir();
|
|
7
8
|
const settingsPath = deps.settingsPath ?? path.join(dataDir, "gateway", "settings.json");
|
|
8
9
|
const auditPath = deps.auditPath ?? path.join(dataDir, "gateway", "audit.jsonl");
|
|
9
10
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
11
|
+
const normalizeOptions = {
|
|
12
|
+
defaultChannel: deps.defaultChannel ?? DEFAULT_GATEWAY_CHANNEL,
|
|
13
|
+
defaultBaseUrl: deps.defaultBaseUrl ?? DEFAULT_BASE_URL
|
|
14
|
+
};
|
|
10
15
|
let cachedSettings = null;
|
|
11
16
|
async function loadSettings() {
|
|
12
17
|
if (cachedSettings) {
|
|
13
18
|
return cachedSettings;
|
|
14
19
|
}
|
|
15
20
|
if (!(await deps.fs.pathExists(settingsPath))) {
|
|
16
|
-
cachedSettings = normalizeSettings({}, now());
|
|
21
|
+
cachedSettings = normalizeSettings({}, now(), normalizeOptions);
|
|
17
22
|
await saveSettings(cachedSettings);
|
|
18
23
|
return cachedSettings;
|
|
19
24
|
}
|
|
20
|
-
cachedSettings = normalizeSettings(await deps.fs.readJson(settingsPath), now());
|
|
25
|
+
cachedSettings = normalizeSettings(await deps.fs.readJson(settingsPath), now(), normalizeOptions);
|
|
21
26
|
return cachedSettings;
|
|
22
27
|
}
|
|
23
28
|
async function saveSettings(settings) {
|
|
24
|
-
cachedSettings = normalizeSettings(settings, now());
|
|
29
|
+
cachedSettings = normalizeSettings(settings, now(), normalizeOptions);
|
|
25
30
|
await deps.fs.writeJsonAtomic(settingsPath, cachedSettings);
|
|
26
31
|
return cachedSettings;
|
|
27
32
|
}
|
|
@@ -32,9 +37,19 @@ export function createGatewaySettingsService(deps) {
|
|
|
32
37
|
return saveSettings({
|
|
33
38
|
...current,
|
|
34
39
|
enabled: input.enabled ?? current.enabled,
|
|
40
|
+
channel: input.channel ?? current.channel,
|
|
35
41
|
translationEnabled: input.translationEnabled ?? current.translationEnabled,
|
|
36
42
|
currentProjectId: input.currentProjectId !== undefined ? normalizeNullableString(input.currentProjectId) : current.currentProjectId,
|
|
37
43
|
currentTaskSlug: input.currentTaskSlug !== undefined ? normalizeNullableString(input.currentTaskSlug) : current.currentTaskSlug,
|
|
44
|
+
binding: {
|
|
45
|
+
...current.binding,
|
|
46
|
+
baseUrl: input.baseUrl !== undefined
|
|
47
|
+
? normalizeBaseUrl(input.baseUrl, normalizeOptions.defaultBaseUrl)
|
|
48
|
+
: current.binding.baseUrl,
|
|
49
|
+
appId: input.larkAppId !== undefined ? normalizeNullableString(input.larkAppId) : current.binding.appId,
|
|
50
|
+
appSecret: input.larkAppSecret !== undefined ? normalizeNullableString(input.larkAppSecret) : current.binding.appSecret,
|
|
51
|
+
homeChatId: input.larkHomeChatId !== undefined ? normalizeNullableString(input.larkHomeChatId) : current.binding.homeChatId
|
|
52
|
+
},
|
|
38
53
|
updatedAt: now()
|
|
39
54
|
});
|
|
40
55
|
},
|
|
@@ -44,7 +59,13 @@ export function createGatewaySettingsService(deps) {
|
|
|
44
59
|
return saveSettings({
|
|
45
60
|
...current,
|
|
46
61
|
enabled: false,
|
|
47
|
-
binding:
|
|
62
|
+
binding: {
|
|
63
|
+
...createDefaultBinding(normalizeOptions.defaultBaseUrl),
|
|
64
|
+
baseUrl: current.binding.baseUrl || normalizeOptions.defaultBaseUrl,
|
|
65
|
+
appId: current.binding.appId,
|
|
66
|
+
appSecret: current.binding.appSecret,
|
|
67
|
+
homeChatId: current.binding.homeChatId
|
|
68
|
+
},
|
|
48
69
|
dedupe: {
|
|
49
70
|
recentInboundMessageIds: []
|
|
50
71
|
},
|
|
@@ -71,7 +92,12 @@ export function createGatewaySettingsService(deps) {
|
|
|
71
92
|
baseUrl: settings.binding.baseUrl,
|
|
72
93
|
boundUserId: settings.binding.boundUserId,
|
|
73
94
|
loginUserId: settings.binding.loginUserId,
|
|
74
|
-
tokenConfigured: Boolean(settings.binding.token)
|
|
95
|
+
tokenConfigured: Boolean(settings.binding.token),
|
|
96
|
+
appId: settings.binding.appId,
|
|
97
|
+
appIdConfigured: Boolean(settings.binding.appId),
|
|
98
|
+
appSecretConfigured: Boolean(settings.binding.appSecret),
|
|
99
|
+
homeChatId: settings.binding.homeChatId,
|
|
100
|
+
pairingCodeExpiresAt: settings.binding.pairingCodeExpiresAt
|
|
75
101
|
},
|
|
76
102
|
pendingConfirmations: settings.pendingConfirmations,
|
|
77
103
|
lastPollStatus: settings.lastPollStatus,
|
|
@@ -87,7 +113,10 @@ export function createGatewaySettingsService(deps) {
|
|
|
87
113
|
}
|
|
88
114
|
};
|
|
89
115
|
}
|
|
90
|
-
export function normalizeSettings(input, timestamp
|
|
116
|
+
export function normalizeSettings(input, timestamp, options = {
|
|
117
|
+
defaultChannel: DEFAULT_GATEWAY_CHANNEL,
|
|
118
|
+
defaultBaseUrl: DEFAULT_BASE_URL
|
|
119
|
+
}) {
|
|
91
120
|
const bindingInput = isObject(input.binding) ? input.binding : {};
|
|
92
121
|
const dedupeInput = isObject(input.dedupe) ? input.dedupe : {};
|
|
93
122
|
const pendingInput = isObject(input.pendingConfirmations)
|
|
@@ -100,20 +129,28 @@ export function normalizeSettings(input, timestamp) {
|
|
|
100
129
|
return {
|
|
101
130
|
version: 1,
|
|
102
131
|
enabled: input.enabled === true,
|
|
103
|
-
channel:
|
|
132
|
+
channel: normalizeGatewayChannel(input.channel, options.defaultChannel),
|
|
104
133
|
translationEnabled: input.translationEnabled !== false,
|
|
105
134
|
currentProjectId: normalizeNullableString(input.currentProjectId),
|
|
106
135
|
currentTaskSlug: normalizeNullableString(input.currentTaskSlug),
|
|
107
136
|
binding: {
|
|
108
|
-
...createDefaultBinding(),
|
|
137
|
+
...createDefaultBinding(options.defaultBaseUrl),
|
|
109
138
|
accountId: normalizeNullableString(bindingInput.accountId),
|
|
110
|
-
baseUrl: normalizeBaseUrl(bindingInput.baseUrl),
|
|
139
|
+
baseUrl: normalizeBaseUrl(bindingInput.baseUrl, options.defaultBaseUrl),
|
|
111
140
|
boundUserId: normalizeNullableString(bindingInput.boundUserId),
|
|
112
141
|
loginUserId: normalizeNullableString(bindingInput.loginUserId),
|
|
113
142
|
token: normalizeNullableString(bindingInput.token),
|
|
143
|
+
appId: normalizeNullableString(bindingInput.appId),
|
|
144
|
+
appSecret: normalizeNullableString(bindingInput.appSecret),
|
|
145
|
+
homeChatId: normalizeNullableString(bindingInput.homeChatId),
|
|
146
|
+
pairingCode: normalizeNullableString(bindingInput.pairingCode),
|
|
147
|
+
pairingCodeExpiresAt: normalizeNullableString(bindingInput.pairingCodeExpiresAt),
|
|
114
148
|
getUpdatesBuf: typeof bindingInput.getUpdatesBuf === "string" ? bindingInput.getUpdatesBuf : "",
|
|
115
149
|
contextTokens: isObject(bindingInput.contextTokens)
|
|
116
150
|
? normalizeStringRecord(bindingInput.contextTokens)
|
|
151
|
+
: {},
|
|
152
|
+
chatIds: isObject(bindingInput.chatIds)
|
|
153
|
+
? normalizeStringRecord(bindingInput.chatIds)
|
|
117
154
|
: {}
|
|
118
155
|
},
|
|
119
156
|
dedupe: {
|
|
@@ -131,17 +168,26 @@ export function normalizeSettings(input, timestamp) {
|
|
|
131
168
|
updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : timestamp
|
|
132
169
|
};
|
|
133
170
|
}
|
|
134
|
-
function createDefaultBinding() {
|
|
171
|
+
function createDefaultBinding(defaultBaseUrl = DEFAULT_BASE_URL) {
|
|
135
172
|
return {
|
|
136
173
|
accountId: null,
|
|
137
|
-
baseUrl:
|
|
174
|
+
baseUrl: defaultBaseUrl,
|
|
138
175
|
boundUserId: null,
|
|
139
176
|
loginUserId: null,
|
|
140
177
|
token: null,
|
|
178
|
+
appId: null,
|
|
179
|
+
appSecret: null,
|
|
180
|
+
homeChatId: null,
|
|
181
|
+
pairingCode: null,
|
|
182
|
+
pairingCodeExpiresAt: null,
|
|
141
183
|
getUpdatesBuf: "",
|
|
142
|
-
contextTokens: {}
|
|
184
|
+
contextTokens: {},
|
|
185
|
+
chatIds: {}
|
|
143
186
|
};
|
|
144
187
|
}
|
|
188
|
+
function normalizeGatewayChannel(input, fallback) {
|
|
189
|
+
return input === "weixin-ilink" || input === "lark" ? input : fallback;
|
|
190
|
+
}
|
|
145
191
|
function normalizeCloseTaskConfirmation(input) {
|
|
146
192
|
if (!isObject(input)) {
|
|
147
193
|
return null;
|
|
@@ -222,10 +268,10 @@ function normalizeMessageStatus(input) {
|
|
|
222
268
|
error: normalizeNullableString(input.error) ?? undefined
|
|
223
269
|
};
|
|
224
270
|
}
|
|
225
|
-
function normalizeBaseUrl(input) {
|
|
271
|
+
function normalizeBaseUrl(input, fallback = DEFAULT_BASE_URL) {
|
|
226
272
|
const raw = typeof input === "string" ? input.trim() : "";
|
|
227
273
|
if (!raw) {
|
|
228
|
-
return
|
|
274
|
+
return fallback;
|
|
229
275
|
}
|
|
230
276
|
if (raw.startsWith("http://") || raw.startsWith("https://")) {
|
|
231
277
|
return raw.replace(/\/+$/, "");
|
package/dist/backend/server.js
CHANGED
|
@@ -18,8 +18,10 @@ import { createNodeFileSystemAdapter } from "./adapters/filesystem.js";
|
|
|
18
18
|
import { createNodePtyTerminalRuntime } from "./runtime/node-pty-runtime.js";
|
|
19
19
|
import { registerGatewayRoutes } from "./api/gateway-routes.js";
|
|
20
20
|
import { registerDiagnosticsRoutes } from "./api/diagnostics-routes.js";
|
|
21
|
+
import { createLarkChannel } from "./gateway/channels/lark-channel.js";
|
|
21
22
|
import { createWeixinIlinkChannel } from "./gateway/channels/weixin-ilink-channel.js";
|
|
22
23
|
import { createGatewayAuditLog } from "./gateway/gateway-audit-log.js";
|
|
24
|
+
import { createGatewayChannelRegistry } from "./gateway/gateway-channel.js";
|
|
23
25
|
import { createGatewayService } from "./gateway/gateway-service.js";
|
|
24
26
|
import { createGatewaySettingsService } from "./gateway/gateway-settings-service.js";
|
|
25
27
|
import { createJobGuardService } from "./services/job-guard-service.js";
|
|
@@ -261,7 +263,15 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
261
263
|
projectService,
|
|
262
264
|
appSettings
|
|
263
265
|
});
|
|
264
|
-
const
|
|
266
|
+
const gatewayChannels = createGatewayChannelRegistry([
|
|
267
|
+
createWeixinIlinkChannel(),
|
|
268
|
+
createLarkChannel()
|
|
269
|
+
]);
|
|
270
|
+
const gatewaySettings = createGatewaySettingsService({
|
|
271
|
+
fs,
|
|
272
|
+
defaultChannel: gatewayChannels.defaultChannel.id,
|
|
273
|
+
defaultBaseUrl: gatewayChannels.defaultChannel.defaultBaseUrl
|
|
274
|
+
});
|
|
265
275
|
const gatewayAudit = createGatewayAuditLog({
|
|
266
276
|
fs,
|
|
267
277
|
auditPath: gatewaySettings.getAuditPath()
|
|
@@ -270,7 +280,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
270
280
|
fs,
|
|
271
281
|
settings: gatewaySettings,
|
|
272
282
|
audit: gatewayAudit,
|
|
273
|
-
|
|
283
|
+
channels: gatewayChannels,
|
|
274
284
|
projectService,
|
|
275
285
|
taskService,
|
|
276
286
|
sessionService,
|
|
@@ -266,6 +266,9 @@ export function createHarnessService(deps) {
|
|
|
266
266
|
async getRepositoryDiff(repoRoot, input = {}) {
|
|
267
267
|
return getRepositoryDiffReport(requireRepositoryDiffGit(deps.git), repoRoot, input, now());
|
|
268
268
|
},
|
|
269
|
+
async getRepositoryFileDiff(repoRoot, input) {
|
|
270
|
+
return getRepositoryFileDiffReport(requireRepositoryDiffGit(deps.git), repoRoot, input, now());
|
|
271
|
+
},
|
|
269
272
|
async mergeRepositoryDiffToCurrentBranch(baseRepoRoot, input) {
|
|
270
273
|
return mergeRepositoryDiffToCurrentBranch(requireRepositoryMergeGit(deps.git), baseRepoRoot, input, now());
|
|
271
274
|
},
|
|
@@ -466,6 +469,7 @@ function requireRepositoryDiffGit(git) {
|
|
|
466
469
|
!git.getCommitInfo ||
|
|
467
470
|
!git.getCommitList ||
|
|
468
471
|
!git.getCurrentBranch ||
|
|
472
|
+
!git.getDiff ||
|
|
469
473
|
!git.getHeadCommit ||
|
|
470
474
|
!git.getMergeBase) {
|
|
471
475
|
throw new VcmError({
|
|
@@ -550,6 +554,34 @@ async function getRepositoryDiffReport(git, repoRoot, input, generatedAt) {
|
|
|
550
554
|
warnings
|
|
551
555
|
};
|
|
552
556
|
}
|
|
557
|
+
async function getRepositoryFileDiffReport(git, repoRoot, input, generatedAt) {
|
|
558
|
+
const filePath = normalizeHarnessGitPath(input.path);
|
|
559
|
+
const baseRepoRoot = input.baseRepoRoot;
|
|
560
|
+
const sourceBranch = await git.getCurrentBranch(repoRoot);
|
|
561
|
+
const targetBranch = await git.getCurrentBranch(baseRepoRoot);
|
|
562
|
+
const baseSha = await git.getHeadCommit(baseRepoRoot);
|
|
563
|
+
const headSha = await git.getHeadCommit(repoRoot);
|
|
564
|
+
const rawDiff = await git.getDiff(repoRoot, baseSha, null, [filePath]);
|
|
565
|
+
const files = parseCommitDiffFiles(rawDiff);
|
|
566
|
+
const file = files.find((entry) => entry.path === filePath || entry.oldPath === filePath) ?? files[0] ?? null;
|
|
567
|
+
const warnings = [];
|
|
568
|
+
if (files.length > 1) {
|
|
569
|
+
warnings.push(`Multiple diff entries matched ${filePath}; showing the first entry.`);
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
version: 1,
|
|
573
|
+
repoRoot,
|
|
574
|
+
baseRepoRoot,
|
|
575
|
+
sourceBranch,
|
|
576
|
+
targetBranch,
|
|
577
|
+
baseSha,
|
|
578
|
+
headSha,
|
|
579
|
+
path: filePath,
|
|
580
|
+
generatedAt,
|
|
581
|
+
file,
|
|
582
|
+
warnings
|
|
583
|
+
};
|
|
584
|
+
}
|
|
553
585
|
async function mergeRepositoryDiffToCurrentBranch(git, baseRepoRoot, input, mergedAt) {
|
|
554
586
|
const taskBranch = input.taskBranch.trim();
|
|
555
587
|
if (!taskBranch) {
|