@ubiquity-os/plugin-sdk 3.8.4 → 3.9.0
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 +47 -10
- package/dist/configuration.d.mts +42 -19
- package/dist/configuration.d.ts +42 -19
- package/dist/configuration.js +430 -92
- package/dist/configuration.mjs +428 -91
- package/dist/{context-sqbr2o6i.d.mts → context-Dwl3aRX-.d.mts} +29 -0
- package/dist/{context-BbEmsEct.d.ts → context-zLHgu52i.d.ts} +29 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +575 -224
- package/dist/index.mjs +574 -223
- package/dist/llm.d.mts +7 -43
- package/dist/llm.d.ts +7 -43
- package/dist/llm.js +175 -43
- package/dist/llm.mjs +175 -43
- package/dist/manifest.d.mts +2 -2
- package/dist/manifest.d.ts +2 -2
- package/dist/signature.js +3 -2
- package/dist/signature.mjs +3 -2
- package/package.json +7 -6
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,79 @@
|
|
|
1
1
|
// src/actions.ts
|
|
2
2
|
import * as core from "@actions/core";
|
|
3
|
-
import * as github2 from "@actions/github";
|
|
4
3
|
import { Value as Value3 } from "@sinclair/typebox/value";
|
|
5
|
-
import {
|
|
4
|
+
import { Logs } from "@ubiquity-os/ubiquity-os-logger";
|
|
5
|
+
|
|
6
|
+
// src/error.ts
|
|
7
|
+
import { LogReturn } from "@ubiquity-os/ubiquity-os-logger";
|
|
8
|
+
function getErrorStatus(err) {
|
|
9
|
+
if (!err || typeof err !== "object") return null;
|
|
10
|
+
const candidate = err;
|
|
11
|
+
const directStatus = candidate.status ?? candidate.response?.status;
|
|
12
|
+
if (typeof directStatus === "number" && Number.isFinite(directStatus)) return directStatus;
|
|
13
|
+
if (typeof directStatus === "string" && directStatus.trim()) {
|
|
14
|
+
const parsed = Number.parseInt(directStatus, 10);
|
|
15
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
16
|
+
}
|
|
17
|
+
if (err instanceof Error) {
|
|
18
|
+
const match = /LLM API error:\s*(\d{3})/i.exec(err.message);
|
|
19
|
+
if (match) {
|
|
20
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
21
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function logByStatus(context, message, metadata) {
|
|
27
|
+
const status = getErrorStatus(metadata.err);
|
|
28
|
+
const payload = { ...metadata, ...status ? { status } : {} };
|
|
29
|
+
if (status && status >= 500) return context.logger.error(message, payload);
|
|
30
|
+
if (status && status >= 400) return context.logger.warn(message, payload);
|
|
31
|
+
if (status && status >= 300) return context.logger.debug(message, payload);
|
|
32
|
+
if (status && status >= 200) return context.logger.ok(message, payload);
|
|
33
|
+
if (status && status >= 100) return context.logger.info(message, payload);
|
|
34
|
+
return context.logger.error(message, payload);
|
|
35
|
+
}
|
|
36
|
+
function transformError(context, error) {
|
|
37
|
+
if (error instanceof LogReturn) {
|
|
38
|
+
return error;
|
|
39
|
+
}
|
|
40
|
+
if (error instanceof AggregateError) {
|
|
41
|
+
const message = error.errors.map((err) => {
|
|
42
|
+
if (err instanceof LogReturn) {
|
|
43
|
+
return err.logMessage.raw;
|
|
44
|
+
}
|
|
45
|
+
if (err instanceof Error) {
|
|
46
|
+
return err.message;
|
|
47
|
+
}
|
|
48
|
+
return String(err);
|
|
49
|
+
}).join("\n\n");
|
|
50
|
+
return logByStatus(context, message, { err: error });
|
|
51
|
+
}
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
return logByStatus(context, error.message, { err: error });
|
|
54
|
+
}
|
|
55
|
+
return logByStatus(context, String(error), { err: error });
|
|
56
|
+
}
|
|
6
57
|
|
|
7
58
|
// src/helpers/runtime-info.ts
|
|
8
|
-
import github from "@actions/github";
|
|
9
59
|
import { getRuntimeKey } from "hono/adapter";
|
|
60
|
+
|
|
61
|
+
// src/helpers/github-context.ts
|
|
62
|
+
import * as github from "@actions/github";
|
|
63
|
+
function getGithubContext() {
|
|
64
|
+
const override = globalThis.__UOS_GITHUB_CONTEXT__;
|
|
65
|
+
if (override) {
|
|
66
|
+
return override;
|
|
67
|
+
}
|
|
68
|
+
const module = github;
|
|
69
|
+
const context = module.context ?? module.default?.context;
|
|
70
|
+
if (!context) {
|
|
71
|
+
throw new Error("GitHub context is unavailable.");
|
|
72
|
+
}
|
|
73
|
+
return context;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/helpers/runtime-info.ts
|
|
10
77
|
var PluginRuntimeInfo = class _PluginRuntimeInfo {
|
|
11
78
|
static _instance = null;
|
|
12
79
|
_env = {};
|
|
@@ -54,10 +121,11 @@ var CfRuntimeInfo = class extends PluginRuntimeInfo {
|
|
|
54
121
|
};
|
|
55
122
|
var NodeRuntimeInfo = class extends PluginRuntimeInfo {
|
|
56
123
|
get version() {
|
|
57
|
-
return
|
|
124
|
+
return getGithubContext().sha;
|
|
58
125
|
}
|
|
59
126
|
get runUrl() {
|
|
60
|
-
|
|
127
|
+
const context = getGithubContext();
|
|
128
|
+
return context.payload.repository ? `${context.payload.repository?.html_url}/actions/runs/${context.runId}` : "http://localhost";
|
|
61
129
|
}
|
|
62
130
|
};
|
|
63
131
|
var DenoRuntimeInfo = class extends PluginRuntimeInfo {
|
|
@@ -150,14 +218,75 @@ function getPluginOptions(options) {
|
|
|
150
218
|
}
|
|
151
219
|
|
|
152
220
|
// src/comment.ts
|
|
221
|
+
var COMMAND_RESPONSE_KIND = "command-response";
|
|
222
|
+
var COMMAND_RESPONSE_MARKER = `"commentKind": "${COMMAND_RESPONSE_KIND}"`;
|
|
223
|
+
var COMMAND_RESPONSE_COMMENT_LIMIT = 50;
|
|
224
|
+
var RECENT_COMMENTS_QUERY = (
|
|
225
|
+
/* GraphQL */
|
|
226
|
+
`
|
|
227
|
+
query ($owner: String!, $repo: String!, $number: Int!, $last: Int!) {
|
|
228
|
+
repository(owner: $owner, name: $repo) {
|
|
229
|
+
issueOrPullRequest(number: $number) {
|
|
230
|
+
__typename
|
|
231
|
+
... on Issue {
|
|
232
|
+
comments(last: $last) {
|
|
233
|
+
nodes {
|
|
234
|
+
id
|
|
235
|
+
body
|
|
236
|
+
isMinimized
|
|
237
|
+
minimizedReason
|
|
238
|
+
author {
|
|
239
|
+
login
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
... on PullRequest {
|
|
245
|
+
comments(last: $last) {
|
|
246
|
+
nodes {
|
|
247
|
+
id
|
|
248
|
+
body
|
|
249
|
+
isMinimized
|
|
250
|
+
minimizedReason
|
|
251
|
+
author {
|
|
252
|
+
login
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
`
|
|
261
|
+
);
|
|
262
|
+
var MINIMIZE_COMMENT_MUTATION = `
|
|
263
|
+
mutation($id: ID!, $classifier: ReportedContentClassifiers!) {
|
|
264
|
+
minimizeComment(input: { subjectId: $id, classifier: $classifier }) {
|
|
265
|
+
minimizedComment {
|
|
266
|
+
isMinimized
|
|
267
|
+
minimizedReason
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
`;
|
|
272
|
+
function logByStatus2(logger, message, status, metadata) {
|
|
273
|
+
const payload = { ...metadata, ...status ? { status } : {} };
|
|
274
|
+
if (status && status >= 500) return logger.error(message, payload);
|
|
275
|
+
if (status && status >= 400) return logger.warn(message, payload);
|
|
276
|
+
if (status && status >= 300) return logger.debug(message, payload);
|
|
277
|
+
if (status && status >= 200) return logger.ok(message, payload);
|
|
278
|
+
if (status && status >= 100) return logger.info(message, payload);
|
|
279
|
+
return logger.error(message, payload);
|
|
280
|
+
}
|
|
153
281
|
var CommentHandler = class _CommentHandler {
|
|
154
282
|
static HEADER_NAME = "UbiquityOS";
|
|
155
283
|
_lastCommentId = { reviewCommentId: null, issueCommentId: null };
|
|
156
|
-
|
|
284
|
+
_commandResponsePolicyApplied = false;
|
|
285
|
+
async _updateIssueComment(context, params) {
|
|
157
286
|
if (!this._lastCommentId.issueCommentId) {
|
|
158
|
-
throw
|
|
287
|
+
throw context.logger.error("issueCommentId is missing");
|
|
159
288
|
}
|
|
160
|
-
const commentData = await
|
|
289
|
+
const commentData = await context.octokit.rest.issues.updateComment({
|
|
161
290
|
owner: params.owner,
|
|
162
291
|
repo: params.repo,
|
|
163
292
|
comment_id: this._lastCommentId.issueCommentId,
|
|
@@ -165,11 +294,11 @@ var CommentHandler = class _CommentHandler {
|
|
|
165
294
|
});
|
|
166
295
|
return { ...commentData.data, issueNumber: params.issueNumber };
|
|
167
296
|
}
|
|
168
|
-
async _updateReviewComment(
|
|
297
|
+
async _updateReviewComment(context, params) {
|
|
169
298
|
if (!this._lastCommentId.reviewCommentId) {
|
|
170
|
-
throw
|
|
299
|
+
throw context.logger.error("reviewCommentId is missing");
|
|
171
300
|
}
|
|
172
|
-
const commentData = await
|
|
301
|
+
const commentData = await context.octokit.rest.pulls.updateReviewComment({
|
|
173
302
|
owner: params.owner,
|
|
174
303
|
repo: params.repo,
|
|
175
304
|
comment_id: this._lastCommentId.reviewCommentId,
|
|
@@ -177,9 +306,9 @@ var CommentHandler = class _CommentHandler {
|
|
|
177
306
|
});
|
|
178
307
|
return { ...commentData.data, issueNumber: params.issueNumber };
|
|
179
308
|
}
|
|
180
|
-
async _createNewComment(
|
|
309
|
+
async _createNewComment(context, params) {
|
|
181
310
|
if (params.commentId) {
|
|
182
|
-
const commentData2 = await
|
|
311
|
+
const commentData2 = await context.octokit.rest.pulls.createReplyForReviewComment({
|
|
183
312
|
owner: params.owner,
|
|
184
313
|
repo: params.repo,
|
|
185
314
|
pull_number: params.issueNumber,
|
|
@@ -189,7 +318,7 @@ var CommentHandler = class _CommentHandler {
|
|
|
189
318
|
this._lastCommentId.reviewCommentId = commentData2.data.id;
|
|
190
319
|
return { ...commentData2.data, issueNumber: params.issueNumber };
|
|
191
320
|
}
|
|
192
|
-
const commentData = await
|
|
321
|
+
const commentData = await context.octokit.rest.issues.createComment({
|
|
193
322
|
owner: params.owner,
|
|
194
323
|
repo: params.repo,
|
|
195
324
|
issue_number: params.issueNumber,
|
|
@@ -198,54 +327,142 @@ var CommentHandler = class _CommentHandler {
|
|
|
198
327
|
this._lastCommentId.issueCommentId = commentData.data.id;
|
|
199
328
|
return { ...commentData.data, issueNumber: params.issueNumber };
|
|
200
329
|
}
|
|
201
|
-
_getIssueNumber(
|
|
202
|
-
if ("issue" in
|
|
203
|
-
if ("pull_request" in
|
|
204
|
-
if ("discussion" in
|
|
330
|
+
_getIssueNumber(context) {
|
|
331
|
+
if ("issue" in context.payload) return context.payload.issue.number;
|
|
332
|
+
if ("pull_request" in context.payload) return context.payload.pull_request.number;
|
|
333
|
+
if ("discussion" in context.payload) return context.payload.discussion.number;
|
|
205
334
|
return void 0;
|
|
206
335
|
}
|
|
207
|
-
_getCommentId(
|
|
208
|
-
return "pull_request" in
|
|
336
|
+
_getCommentId(context) {
|
|
337
|
+
return "pull_request" in context.payload && "comment" in context.payload ? context.payload.comment.id : void 0;
|
|
338
|
+
}
|
|
339
|
+
_getCommentNodeId(context) {
|
|
340
|
+
const payload = context.payload;
|
|
341
|
+
const nodeId = payload.comment?.node_id;
|
|
342
|
+
return typeof nodeId === "string" && nodeId.trim() ? nodeId : null;
|
|
209
343
|
}
|
|
210
|
-
_extractIssueContext(
|
|
211
|
-
if (!("repository" in
|
|
344
|
+
_extractIssueContext(context) {
|
|
345
|
+
if (!("repository" in context.payload) || !context.payload.repository?.owner?.login) {
|
|
212
346
|
return null;
|
|
213
347
|
}
|
|
214
|
-
const issueNumber = this._getIssueNumber(
|
|
348
|
+
const issueNumber = this._getIssueNumber(context);
|
|
215
349
|
if (!issueNumber) return null;
|
|
216
350
|
return {
|
|
217
351
|
issueNumber,
|
|
218
|
-
commentId: this._getCommentId(
|
|
219
|
-
owner:
|
|
220
|
-
repo:
|
|
352
|
+
commentId: this._getCommentId(context),
|
|
353
|
+
owner: context.payload.repository.owner.login,
|
|
354
|
+
repo: context.payload.repository.name
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
_extractIssueLocator(context) {
|
|
358
|
+
if (!("issue" in context.payload) && !("pull_request" in context.payload)) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const issueContext = this._extractIssueContext(context);
|
|
362
|
+
if (!issueContext) return null;
|
|
363
|
+
return {
|
|
364
|
+
owner: issueContext.owner,
|
|
365
|
+
repo: issueContext.repo,
|
|
366
|
+
issueNumber: issueContext.issueNumber
|
|
221
367
|
};
|
|
222
368
|
}
|
|
223
|
-
|
|
369
|
+
_shouldApplyCommandResponsePolicy(context) {
|
|
370
|
+
return Boolean(context.command);
|
|
371
|
+
}
|
|
372
|
+
_isCommandResponseComment(body) {
|
|
373
|
+
return typeof body === "string" && body.includes(COMMAND_RESPONSE_MARKER);
|
|
374
|
+
}
|
|
375
|
+
_getGraphqlClient(context) {
|
|
376
|
+
const graphql = context.octokit.graphql;
|
|
377
|
+
return typeof graphql === "function" ? graphql : null;
|
|
378
|
+
}
|
|
379
|
+
async _fetchRecentComments(context, locator, last = COMMAND_RESPONSE_COMMENT_LIMIT) {
|
|
380
|
+
const graphql = this._getGraphqlClient(context);
|
|
381
|
+
if (!graphql) return [];
|
|
382
|
+
try {
|
|
383
|
+
const data = await graphql(RECENT_COMMENTS_QUERY, {
|
|
384
|
+
owner: locator.owner,
|
|
385
|
+
repo: locator.repo,
|
|
386
|
+
number: locator.issueNumber,
|
|
387
|
+
last
|
|
388
|
+
});
|
|
389
|
+
const nodes = data.repository?.issueOrPullRequest?.comments?.nodes ?? [];
|
|
390
|
+
return nodes.filter((node) => Boolean(node));
|
|
391
|
+
} catch (error) {
|
|
392
|
+
context.logger.debug("Failed to fetch recent comments (non-fatal)", { err: error });
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
_findPreviousCommandResponseComment(comments, currentCommentId) {
|
|
397
|
+
for (let idx = comments.length - 1; idx >= 0; idx -= 1) {
|
|
398
|
+
const comment = comments[idx];
|
|
399
|
+
if (!comment) continue;
|
|
400
|
+
if (currentCommentId && comment.id === currentCommentId) continue;
|
|
401
|
+
if (this._isCommandResponseComment(comment.body)) {
|
|
402
|
+
return comment;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
async _minimizeComment(context, commentNodeId, classifier = "RESOLVED") {
|
|
408
|
+
const graphql = this._getGraphqlClient(context);
|
|
409
|
+
if (!graphql) return;
|
|
410
|
+
try {
|
|
411
|
+
await graphql(MINIMIZE_COMMENT_MUTATION, {
|
|
412
|
+
id: commentNodeId,
|
|
413
|
+
classifier
|
|
414
|
+
});
|
|
415
|
+
} catch (error) {
|
|
416
|
+
context.logger.debug("Failed to minimize comment (non-fatal)", { err: error, commentNodeId });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async _applyCommandResponsePolicy(context) {
|
|
420
|
+
if (this._commandResponsePolicyApplied) return;
|
|
421
|
+
this._commandResponsePolicyApplied = true;
|
|
422
|
+
if (!this._shouldApplyCommandResponsePolicy(context)) return;
|
|
423
|
+
const locator = this._extractIssueLocator(context);
|
|
424
|
+
const commentNodeId = this._getCommentNodeId(context);
|
|
425
|
+
const comments = locator ? await this._fetchRecentComments(context, locator) : [];
|
|
426
|
+
const current = commentNodeId ? comments.find((comment) => comment.id === commentNodeId) : null;
|
|
427
|
+
const isCurrentMinimized = current?.isMinimized ?? false;
|
|
428
|
+
if (commentNodeId && !isCurrentMinimized) {
|
|
429
|
+
await this._minimizeComment(context, commentNodeId);
|
|
430
|
+
}
|
|
431
|
+
const previous = this._findPreviousCommandResponseComment(comments, commentNodeId);
|
|
432
|
+
if (previous && !previous.isMinimized) {
|
|
433
|
+
await this._minimizeComment(context, previous.id);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
_processMessage(context, message) {
|
|
224
437
|
if (message instanceof Error) {
|
|
225
438
|
const metadata2 = {
|
|
226
439
|
message: message.message,
|
|
227
440
|
name: message.name,
|
|
228
441
|
stack: message.stack
|
|
229
442
|
};
|
|
230
|
-
|
|
443
|
+
const status = getErrorStatus(message);
|
|
444
|
+
const logReturn = logByStatus2(context.logger, message.message, status, metadata2);
|
|
445
|
+
return { metadata: { ...metadata2, ...status ? { status } : {} }, logMessage: logReturn.logMessage };
|
|
231
446
|
}
|
|
447
|
+
const stackLine = message.metadata?.error?.stack?.split("\n")[2];
|
|
448
|
+
const callerMatch = stackLine ? /at (\S+)/.exec(stackLine) : null;
|
|
232
449
|
const metadata = message.metadata ? {
|
|
233
450
|
...message.metadata,
|
|
234
451
|
message: message.metadata.message,
|
|
235
452
|
stack: message.metadata.stack || message.metadata.error?.stack,
|
|
236
|
-
caller: message.metadata.caller ||
|
|
453
|
+
caller: message.metadata.caller || callerMatch?.[1]
|
|
237
454
|
} : { ...message };
|
|
238
455
|
return { metadata, logMessage: message.logMessage };
|
|
239
456
|
}
|
|
240
|
-
_getInstigatorName(
|
|
241
|
-
if ("installation" in
|
|
242
|
-
return
|
|
457
|
+
_getInstigatorName(context) {
|
|
458
|
+
if ("installation" in context.payload && context.payload.installation && "account" in context.payload.installation && context.payload.installation?.account?.name) {
|
|
459
|
+
return context.payload.installation?.account?.name;
|
|
243
460
|
}
|
|
244
|
-
return
|
|
461
|
+
return context.payload.sender?.login || _CommentHandler.HEADER_NAME;
|
|
245
462
|
}
|
|
246
|
-
_createMetadataContent(
|
|
463
|
+
_createMetadataContent(context, metadata) {
|
|
247
464
|
const jsonPretty = sanitizeMetadata(metadata);
|
|
248
|
-
const instigatorName = this._getInstigatorName(
|
|
465
|
+
const instigatorName = this._getInstigatorName(context);
|
|
249
466
|
const runUrl = PluginRuntimeInfo.getInstance().runUrl;
|
|
250
467
|
const version = PluginRuntimeInfo.getInstance().version;
|
|
251
468
|
const callingFnName = metadata.caller || "anonymous";
|
|
@@ -262,64 +479,46 @@ var CommentHandler = class _CommentHandler {
|
|
|
262
479
|
/*
|
|
263
480
|
* Creates the body for the comment, embeds the metadata and the header hidden in the body as well.
|
|
264
481
|
*/
|
|
265
|
-
createCommentBody(
|
|
266
|
-
return this._createCommentBody(
|
|
482
|
+
createCommentBody(context, message, options) {
|
|
483
|
+
return this._createCommentBody(context, message, options);
|
|
267
484
|
}
|
|
268
|
-
_createCommentBody(
|
|
269
|
-
const { metadata, logMessage } = this._processMessage(
|
|
270
|
-
const
|
|
485
|
+
_createCommentBody(context, message, options) {
|
|
486
|
+
const { metadata, logMessage } = this._processMessage(context, message);
|
|
487
|
+
const shouldTagCommandResponse = options?.commentKind && typeof metadata === "object" && !("commentKind" in metadata);
|
|
488
|
+
const metadataWithKind = shouldTagCommandResponse ? { ...metadata, commentKind: options?.commentKind } : metadata;
|
|
489
|
+
const { header, jsonPretty } = this._createMetadataContent(context, metadataWithKind);
|
|
271
490
|
const metadataContent = this._formatMetadataContent(logMessage, header, jsonPretty);
|
|
272
491
|
return `${options?.raw ? logMessage?.raw : logMessage?.diff}
|
|
273
492
|
|
|
274
493
|
${metadataContent}
|
|
275
494
|
`;
|
|
276
495
|
}
|
|
277
|
-
async postComment(
|
|
278
|
-
|
|
496
|
+
async postComment(context, message, options = { updateComment: true, raw: false }) {
|
|
497
|
+
await this._applyCommandResponsePolicy(context);
|
|
498
|
+
const issueContext = this._extractIssueContext(context);
|
|
279
499
|
if (!issueContext) {
|
|
280
|
-
|
|
500
|
+
context.logger.warn("Cannot post comment: missing issue context in payload");
|
|
281
501
|
return null;
|
|
282
502
|
}
|
|
283
|
-
const
|
|
503
|
+
const shouldTagCommandResponse = this._shouldApplyCommandResponsePolicy(context);
|
|
504
|
+
const body = this._createCommentBody(context, message, {
|
|
505
|
+
...options,
|
|
506
|
+
commentKind: options.commentKind ?? (shouldTagCommandResponse ? COMMAND_RESPONSE_KIND : void 0)
|
|
507
|
+
});
|
|
284
508
|
const { issueNumber, commentId, owner, repo } = issueContext;
|
|
285
509
|
const params = { owner, repo, body, issueNumber };
|
|
286
510
|
if (options.updateComment) {
|
|
287
|
-
if (this._lastCommentId.issueCommentId && !("pull_request" in
|
|
288
|
-
return this._updateIssueComment(
|
|
511
|
+
if (this._lastCommentId.issueCommentId && !("pull_request" in context.payload && "comment" in context.payload)) {
|
|
512
|
+
return this._updateIssueComment(context, params);
|
|
289
513
|
}
|
|
290
|
-
if (this._lastCommentId.reviewCommentId && "pull_request" in
|
|
291
|
-
return this._updateReviewComment(
|
|
514
|
+
if (this._lastCommentId.reviewCommentId && "pull_request" in context.payload && "comment" in context.payload) {
|
|
515
|
+
return this._updateReviewComment(context, params);
|
|
292
516
|
}
|
|
293
517
|
}
|
|
294
|
-
return this._createNewComment(
|
|
518
|
+
return this._createNewComment(context, { ...params, commentId });
|
|
295
519
|
}
|
|
296
520
|
};
|
|
297
521
|
|
|
298
|
-
// src/error.ts
|
|
299
|
-
import { LogReturn as LogReturn2 } from "@ubiquity-os/ubiquity-os-logger";
|
|
300
|
-
function transformError(context2, error) {
|
|
301
|
-
let loggerError;
|
|
302
|
-
if (error instanceof AggregateError) {
|
|
303
|
-
loggerError = context2.logger.error(
|
|
304
|
-
error.errors.map((err) => {
|
|
305
|
-
if (err instanceof LogReturn2) {
|
|
306
|
-
return err.logMessage.raw;
|
|
307
|
-
} else if (err instanceof Error) {
|
|
308
|
-
return err.message;
|
|
309
|
-
} else {
|
|
310
|
-
return err;
|
|
311
|
-
}
|
|
312
|
-
}).join("\n\n"),
|
|
313
|
-
{ error }
|
|
314
|
-
);
|
|
315
|
-
} else if (error instanceof Error || error instanceof LogReturn2) {
|
|
316
|
-
loggerError = error;
|
|
317
|
-
} else {
|
|
318
|
-
loggerError = context2.logger.error(String(error));
|
|
319
|
-
}
|
|
320
|
-
return loggerError;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
522
|
// src/helpers/command.ts
|
|
324
523
|
import { Value } from "@sinclair/typebox/value";
|
|
325
524
|
function getCommand(inputs, pluginOptions) {
|
|
@@ -390,7 +589,7 @@ async function verifySignature(publicKeyPem, inputs, signature) {
|
|
|
390
589
|
ref: inputs.ref,
|
|
391
590
|
command: inputs.command
|
|
392
591
|
};
|
|
393
|
-
const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").
|
|
592
|
+
const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace(/\s+/g, "");
|
|
394
593
|
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
395
594
|
const publicKey = await crypto.subtle.importKey(
|
|
396
595
|
"spki",
|
|
@@ -442,90 +641,110 @@ var inputSchema = T2.Object({
|
|
|
442
641
|
});
|
|
443
642
|
|
|
444
643
|
// src/actions.ts
|
|
445
|
-
async function handleError(
|
|
644
|
+
async function handleError(context, pluginOptions, error) {
|
|
446
645
|
console.error(error);
|
|
447
|
-
const loggerError = transformError(
|
|
448
|
-
|
|
449
|
-
core.setFailed(loggerError.logMessage.diff);
|
|
450
|
-
} else if (loggerError instanceof Error) {
|
|
451
|
-
core.setFailed(loggerError);
|
|
452
|
-
}
|
|
646
|
+
const loggerError = transformError(context, error);
|
|
647
|
+
core.setFailed(loggerError.logMessage.diff);
|
|
453
648
|
if (pluginOptions.postCommentOnError && loggerError) {
|
|
454
|
-
await
|
|
649
|
+
await context.commentHandler.postComment(context, loggerError);
|
|
455
650
|
}
|
|
456
651
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
if (!
|
|
652
|
+
function getDispatchTokenOrFail(pluginOptions) {
|
|
653
|
+
if (!pluginOptions.returnDataToKernel) return null;
|
|
654
|
+
const token = process.env.PLUGIN_GITHUB_TOKEN;
|
|
655
|
+
if (!token) {
|
|
461
656
|
core.setFailed("Error: PLUGIN_GITHUB_TOKEN env is not set");
|
|
462
|
-
return;
|
|
657
|
+
return null;
|
|
463
658
|
}
|
|
464
|
-
|
|
659
|
+
return token;
|
|
660
|
+
}
|
|
661
|
+
async function getInputsOrFail(pluginOptions) {
|
|
662
|
+
const githubContext = getGithubContext();
|
|
663
|
+
const body = githubContext.payload.inputs;
|
|
465
664
|
const inputSchemaErrors = [...Value3.Errors(inputSchema, body)];
|
|
466
665
|
if (inputSchemaErrors.length) {
|
|
467
666
|
console.dir(inputSchemaErrors, { depth: null });
|
|
468
667
|
core.setFailed(`Error: Invalid inputs payload: ${inputSchemaErrors.map((o) => o.message).join(", ")}`);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
const signature = body.signature;
|
|
472
|
-
if (!pluginOptions.bypassSignatureVerification && !await verifySignature(pluginOptions.kernelPublicKey, body, signature)) {
|
|
473
|
-
core.setFailed(`Error: Invalid signature`);
|
|
474
|
-
return;
|
|
668
|
+
return null;
|
|
475
669
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
} catch (e) {
|
|
482
|
-
console.dir(...Value3.Errors(pluginOptions.settingsSchema, inputs.settings), { depth: null });
|
|
483
|
-
core.setFailed(`Error: Invalid settings provided.`);
|
|
484
|
-
throw e;
|
|
670
|
+
if (!pluginOptions.bypassSignatureVerification) {
|
|
671
|
+
const signature = typeof body.signature === "string" ? body.signature : "";
|
|
672
|
+
if (!signature) {
|
|
673
|
+
core.setFailed("Error: Missing signature");
|
|
674
|
+
return null;
|
|
485
675
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (pluginOptions.envSchema) {
|
|
491
|
-
try {
|
|
492
|
-
env = Value3.Decode(pluginOptions.envSchema, Value3.Default(pluginOptions.envSchema, process.env));
|
|
493
|
-
} catch (e) {
|
|
494
|
-
console.dir(...Value3.Errors(pluginOptions.envSchema, process.env), { depth: null });
|
|
495
|
-
core.setFailed(`Error: Invalid environment provided.`);
|
|
496
|
-
throw e;
|
|
676
|
+
const isValid = await verifySignature(pluginOptions.kernelPublicKey, body, signature);
|
|
677
|
+
if (!isValid) {
|
|
678
|
+
core.setFailed("Error: Invalid signature");
|
|
679
|
+
return null;
|
|
497
680
|
}
|
|
498
|
-
} else {
|
|
499
|
-
env = process.env;
|
|
500
681
|
}
|
|
501
|
-
|
|
502
|
-
|
|
682
|
+
return Value3.Decode(inputSchema, body);
|
|
683
|
+
}
|
|
684
|
+
function decodeWithSchema(schema, value, errorMessage) {
|
|
685
|
+
if (!schema) {
|
|
686
|
+
return { value };
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
return { value: Value3.Decode(schema, Value3.Default(schema, value)) };
|
|
690
|
+
} catch (error) {
|
|
691
|
+
console.dir(...Value3.Errors(schema, value), { depth: null });
|
|
692
|
+
const err = new Error(errorMessage);
|
|
693
|
+
err.cause = error;
|
|
694
|
+
return { value: null, error: err };
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async function createActionsPlugin(handler, options) {
|
|
698
|
+
const pluginOptions = getPluginOptions(options);
|
|
699
|
+
const pluginGithubToken = getDispatchTokenOrFail(pluginOptions);
|
|
700
|
+
if (pluginOptions.returnDataToKernel && !pluginGithubToken) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const inputs = await getInputsOrFail(pluginOptions);
|
|
704
|
+
if (!inputs) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const context = {
|
|
503
708
|
eventName: inputs.eventName,
|
|
504
709
|
payload: inputs.eventPayload,
|
|
505
|
-
command,
|
|
710
|
+
command: null,
|
|
506
711
|
authToken: inputs.authToken,
|
|
507
712
|
ubiquityKernelToken: inputs.ubiquityKernelToken,
|
|
508
713
|
octokit: new customOctokit({ auth: inputs.authToken }),
|
|
509
|
-
config,
|
|
510
|
-
env,
|
|
714
|
+
config: inputs.settings,
|
|
715
|
+
env: process.env,
|
|
511
716
|
logger: new Logs(pluginOptions.logLevel),
|
|
512
717
|
commentHandler: new CommentHandler()
|
|
513
718
|
};
|
|
719
|
+
const configResult = decodeWithSchema(pluginOptions.settingsSchema, inputs.settings, "Error: Invalid settings provided.");
|
|
720
|
+
if (!configResult.value) {
|
|
721
|
+
await handleError(context, pluginOptions, configResult.error ?? new Error("Error: Invalid settings provided."));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
context.config = configResult.value;
|
|
725
|
+
const envResult = decodeWithSchema(pluginOptions.envSchema, process.env, "Error: Invalid environment provided.");
|
|
726
|
+
if (!envResult.value) {
|
|
727
|
+
await handleError(context, pluginOptions, envResult.error ?? new Error("Error: Invalid environment provided."));
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
context.env = envResult.value;
|
|
514
731
|
try {
|
|
515
|
-
|
|
732
|
+
context.command = getCommand(inputs, pluginOptions);
|
|
733
|
+
const result = await handler(context);
|
|
516
734
|
core.setOutput("result", result);
|
|
517
|
-
if (pluginOptions
|
|
735
|
+
if (pluginOptions.returnDataToKernel && pluginGithubToken) {
|
|
518
736
|
await returnDataToKernel(pluginGithubToken, inputs.stateId, result);
|
|
519
737
|
}
|
|
520
738
|
} catch (error) {
|
|
521
|
-
await handleError(
|
|
739
|
+
await handleError(context, pluginOptions, error);
|
|
522
740
|
}
|
|
523
741
|
}
|
|
524
742
|
async function returnDataToKernel(repoToken, stateId, output) {
|
|
743
|
+
const githubContext = getGithubContext();
|
|
525
744
|
const octokit = new customOctokit({ auth: repoToken });
|
|
526
745
|
await octokit.rest.repos.createDispatchEvent({
|
|
527
|
-
owner:
|
|
528
|
-
repo:
|
|
746
|
+
owner: githubContext.repo.owner,
|
|
747
|
+
repo: githubContext.repo.repo,
|
|
529
748
|
event_type: "return-data-to-ubiquity-os-kernel",
|
|
530
749
|
client_payload: {
|
|
531
750
|
state_id: stateId,
|
|
@@ -587,107 +806,17 @@ function processSegment(segment, extraTags, shouldCollapseEmptyLines) {
|
|
|
587
806
|
return s;
|
|
588
807
|
}
|
|
589
808
|
|
|
590
|
-
// src/llm/index.ts
|
|
591
|
-
function normalizeBaseUrl(baseUrl) {
|
|
592
|
-
let normalized = baseUrl.trim();
|
|
593
|
-
while (normalized.endsWith("/")) {
|
|
594
|
-
normalized = normalized.slice(0, -1);
|
|
595
|
-
}
|
|
596
|
-
return normalized;
|
|
597
|
-
}
|
|
598
|
-
function getEnvString(name) {
|
|
599
|
-
if (typeof process === "undefined" || !process?.env) return "";
|
|
600
|
-
return String(process.env[name] ?? "").trim();
|
|
601
|
-
}
|
|
602
|
-
function getAiBaseUrl(options) {
|
|
603
|
-
if (typeof options.baseUrl === "string" && options.baseUrl.trim()) {
|
|
604
|
-
return normalizeBaseUrl(options.baseUrl);
|
|
605
|
-
}
|
|
606
|
-
const envBaseUrl = getEnvString("UBQ_AI_BASE_URL") || getEnvString("UBQ_AI_URL");
|
|
607
|
-
if (envBaseUrl) return normalizeBaseUrl(envBaseUrl);
|
|
608
|
-
return "https://ai.ubq.fi";
|
|
609
|
-
}
|
|
610
|
-
async function callLlm(options, input) {
|
|
611
|
-
const inputPayload = input;
|
|
612
|
-
const authToken = inputPayload.authToken;
|
|
613
|
-
const ubiquityKernelToken = inputPayload.ubiquityKernelToken;
|
|
614
|
-
const payload = inputPayload.payload ?? inputPayload.eventPayload;
|
|
615
|
-
const owner = payload?.repository?.owner?.login ?? "";
|
|
616
|
-
const repo = payload?.repository?.name ?? "";
|
|
617
|
-
const installationId = payload?.installation?.id;
|
|
618
|
-
if (!authToken) throw new Error("Missing authToken in inputs");
|
|
619
|
-
const isKernelTokenRequired = authToken.trim().startsWith("gh");
|
|
620
|
-
if (isKernelTokenRequired && !ubiquityKernelToken) {
|
|
621
|
-
throw new Error("Missing ubiquityKernelToken in inputs (kernel attestation is required for GitHub auth)");
|
|
622
|
-
}
|
|
623
|
-
const { baseUrl, model, stream: isStream, messages, ...rest } = options;
|
|
624
|
-
const url = `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
|
|
625
|
-
const body = JSON.stringify({
|
|
626
|
-
...rest,
|
|
627
|
-
...model ? { model } : {},
|
|
628
|
-
messages,
|
|
629
|
-
stream: isStream ?? false
|
|
630
|
-
});
|
|
631
|
-
const headers = {
|
|
632
|
-
Authorization: `Bearer ${authToken}`,
|
|
633
|
-
"Content-Type": "application/json"
|
|
634
|
-
};
|
|
635
|
-
if (owner) headers["X-GitHub-Owner"] = owner;
|
|
636
|
-
if (repo) headers["X-GitHub-Repo"] = repo;
|
|
637
|
-
if (typeof installationId === "number" && Number.isFinite(installationId)) {
|
|
638
|
-
headers["X-GitHub-Installation-Id"] = String(installationId);
|
|
639
|
-
}
|
|
640
|
-
if (ubiquityKernelToken) {
|
|
641
|
-
headers["X-Ubiquity-Kernel-Token"] = ubiquityKernelToken;
|
|
642
|
-
}
|
|
643
|
-
const response = await fetch(url, { method: "POST", headers, body });
|
|
644
|
-
if (!response.ok) {
|
|
645
|
-
const err = await response.text();
|
|
646
|
-
throw new Error(`LLM API error: ${response.status} - ${err}`);
|
|
647
|
-
}
|
|
648
|
-
if (isStream) {
|
|
649
|
-
if (!response.body) {
|
|
650
|
-
throw new Error("LLM API error: missing response body for streaming request");
|
|
651
|
-
}
|
|
652
|
-
return parseSseStream(response.body);
|
|
653
|
-
}
|
|
654
|
-
return response.json();
|
|
655
|
-
}
|
|
656
|
-
async function* parseSseStream(body) {
|
|
657
|
-
const reader = body.getReader();
|
|
658
|
-
const decoder = new TextDecoder();
|
|
659
|
-
let buffer = "";
|
|
660
|
-
try {
|
|
661
|
-
while (true) {
|
|
662
|
-
const { value, done: isDone } = await reader.read();
|
|
663
|
-
if (isDone) break;
|
|
664
|
-
buffer += decoder.decode(value, { stream: true });
|
|
665
|
-
const events = buffer.split("\n\n");
|
|
666
|
-
buffer = events.pop() || "";
|
|
667
|
-
for (const event of events) {
|
|
668
|
-
if (event.startsWith("data: ")) {
|
|
669
|
-
const data = event.slice(6);
|
|
670
|
-
if (data === "[DONE]") return;
|
|
671
|
-
yield JSON.parse(data);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
} finally {
|
|
676
|
-
reader.releaseLock();
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
809
|
// src/server.ts
|
|
681
810
|
import { Value as Value4 } from "@sinclair/typebox/value";
|
|
682
811
|
import { Logs as Logs2 } from "@ubiquity-os/ubiquity-os-logger";
|
|
683
812
|
import { Hono } from "hono";
|
|
684
813
|
import { env as honoEnv } from "hono/adapter";
|
|
685
814
|
import { HTTPException } from "hono/http-exception";
|
|
686
|
-
async function handleError2(
|
|
815
|
+
async function handleError2(context, pluginOptions, error) {
|
|
687
816
|
console.error(error);
|
|
688
|
-
const loggerError = transformError(
|
|
817
|
+
const loggerError = transformError(context, error);
|
|
689
818
|
if (pluginOptions.postCommentOnError && loggerError) {
|
|
690
|
-
await
|
|
819
|
+
await context.commentHandler.postComment(context, loggerError);
|
|
691
820
|
}
|
|
692
821
|
throw new HTTPException(500, { message: "Unexpected error" });
|
|
693
822
|
}
|
|
@@ -738,7 +867,7 @@ function createPlugin(handler, manifest, options) {
|
|
|
738
867
|
const workerName = new URL(inputs.ref).hostname.split(".")[0];
|
|
739
868
|
PluginRuntimeInfo.getInstance({ ...env, CLOUDFLARE_WORKER_NAME: workerName });
|
|
740
869
|
const command = getCommand(inputs, pluginOptions);
|
|
741
|
-
const
|
|
870
|
+
const context = {
|
|
742
871
|
eventName: inputs.eventName,
|
|
743
872
|
payload: inputs.eventPayload,
|
|
744
873
|
command,
|
|
@@ -751,14 +880,236 @@ function createPlugin(handler, manifest, options) {
|
|
|
751
880
|
commentHandler: new CommentHandler()
|
|
752
881
|
};
|
|
753
882
|
try {
|
|
754
|
-
const result = await handler(
|
|
883
|
+
const result = await handler(context);
|
|
755
884
|
return ctx.json({ stateId: inputs.stateId, output: result ?? {} });
|
|
756
885
|
} catch (error) {
|
|
757
|
-
await handleError2(
|
|
886
|
+
await handleError2(context, pluginOptions, error);
|
|
758
887
|
}
|
|
759
888
|
});
|
|
760
889
|
return app;
|
|
761
890
|
}
|
|
891
|
+
|
|
892
|
+
// src/llm/index.ts
|
|
893
|
+
var EMPTY_STRING = "";
|
|
894
|
+
function normalizeBaseUrl(baseUrl) {
|
|
895
|
+
let normalized = baseUrl.trim();
|
|
896
|
+
while (normalized.endsWith("/")) {
|
|
897
|
+
normalized = normalized.slice(0, -1);
|
|
898
|
+
}
|
|
899
|
+
return normalized;
|
|
900
|
+
}
|
|
901
|
+
var MAX_LLM_RETRIES = 2;
|
|
902
|
+
var RETRY_BACKOFF_MS = [250, 750];
|
|
903
|
+
function getRetryDelayMs(attempt) {
|
|
904
|
+
return RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)] ?? 750;
|
|
905
|
+
}
|
|
906
|
+
function sleep(ms) {
|
|
907
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
908
|
+
}
|
|
909
|
+
function getEnvString(name) {
|
|
910
|
+
if (typeof process === "undefined" || !process?.env) return EMPTY_STRING;
|
|
911
|
+
return String(process.env[name] ?? EMPTY_STRING).trim();
|
|
912
|
+
}
|
|
913
|
+
function normalizeToken(value) {
|
|
914
|
+
return typeof value === "string" ? value.trim() : EMPTY_STRING;
|
|
915
|
+
}
|
|
916
|
+
function isGitHubToken(token) {
|
|
917
|
+
return token.trim().startsWith("gh");
|
|
918
|
+
}
|
|
919
|
+
function getEnvTokenFromInput(input) {
|
|
920
|
+
if ("env" in input) {
|
|
921
|
+
const envValue = input.env;
|
|
922
|
+
if (envValue && typeof envValue === "object") {
|
|
923
|
+
const token = normalizeToken(envValue.UOS_AI_TOKEN);
|
|
924
|
+
if (token) return token;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return getEnvString("UOS_AI_TOKEN");
|
|
928
|
+
}
|
|
929
|
+
function resolveAuthToken(input, aiAuthToken) {
|
|
930
|
+
const explicit = normalizeToken(aiAuthToken);
|
|
931
|
+
if (explicit) return { token: explicit, isGitHub: isGitHubToken(explicit) };
|
|
932
|
+
const envToken = getEnvTokenFromInput(input);
|
|
933
|
+
if (envToken) return { token: envToken, isGitHub: isGitHubToken(envToken) };
|
|
934
|
+
const fallback = normalizeToken(input.authToken);
|
|
935
|
+
if (!fallback) {
|
|
936
|
+
const err = new Error("Missing auth token; set UOS_AI_TOKEN, pass aiAuthToken, or provide authToken in input");
|
|
937
|
+
err.status = 401;
|
|
938
|
+
throw err;
|
|
939
|
+
}
|
|
940
|
+
return { token: fallback, isGitHub: isGitHubToken(fallback) };
|
|
941
|
+
}
|
|
942
|
+
function getAiBaseUrl(options) {
|
|
943
|
+
if (typeof options.baseUrl === "string" && options.baseUrl.trim()) {
|
|
944
|
+
return normalizeBaseUrl(options.baseUrl);
|
|
945
|
+
}
|
|
946
|
+
const envBaseUrl = getEnvString("UOS_AI_URL");
|
|
947
|
+
if (envBaseUrl) return normalizeBaseUrl(envBaseUrl);
|
|
948
|
+
return "https://ai.ubq.fi";
|
|
949
|
+
}
|
|
950
|
+
async function callLlm(options, input) {
|
|
951
|
+
const { baseUrl, model, stream: isStream, messages, aiAuthToken, ...rest } = options;
|
|
952
|
+
const { token: authToken, isGitHub } = resolveAuthToken(input, aiAuthToken);
|
|
953
|
+
const kernelToken = "ubiquityKernelToken" in input ? input.ubiquityKernelToken : void 0;
|
|
954
|
+
const payload = getPayload(input);
|
|
955
|
+
const { owner, repo, installationId } = getRepoMetadata(payload);
|
|
956
|
+
ensureMessages(messages);
|
|
957
|
+
const url = buildAiUrl(options, baseUrl);
|
|
958
|
+
const body = JSON.stringify({
|
|
959
|
+
...rest,
|
|
960
|
+
...model ? { model } : {},
|
|
961
|
+
messages,
|
|
962
|
+
stream: isStream ?? false
|
|
963
|
+
});
|
|
964
|
+
const headers = buildHeaders(authToken, {
|
|
965
|
+
owner,
|
|
966
|
+
repo,
|
|
967
|
+
installationId,
|
|
968
|
+
ubiquityKernelToken: isGitHub ? kernelToken : void 0
|
|
969
|
+
});
|
|
970
|
+
const response = await fetchWithRetry(url, { method: "POST", headers, body }, MAX_LLM_RETRIES);
|
|
971
|
+
if (isStream) {
|
|
972
|
+
if (!response.body) {
|
|
973
|
+
throw new Error("LLM API error: missing response body for streaming request");
|
|
974
|
+
}
|
|
975
|
+
return parseSseStream(response.body);
|
|
976
|
+
}
|
|
977
|
+
const rawText = await response.text();
|
|
978
|
+
try {
|
|
979
|
+
return JSON.parse(rawText);
|
|
980
|
+
} catch (err) {
|
|
981
|
+
const preview = rawText ? rawText.slice(0, 1e3) : EMPTY_STRING;
|
|
982
|
+
const message = "LLM API error: failed to parse JSON response from server" + (preview ? `; response body (truncated): ${preview}` : EMPTY_STRING);
|
|
983
|
+
const error = new Error(message);
|
|
984
|
+
error.cause = err;
|
|
985
|
+
error.status = response.status;
|
|
986
|
+
throw error;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function ensureMessages(messages) {
|
|
990
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
991
|
+
const err = new Error("messages must be a non-empty array");
|
|
992
|
+
err.status = 400;
|
|
993
|
+
throw err;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function buildAiUrl(options, baseUrl) {
|
|
997
|
+
return `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
|
|
998
|
+
}
|
|
999
|
+
async function fetchWithRetry(url, options, maxRetries) {
|
|
1000
|
+
let attempt = 0;
|
|
1001
|
+
let lastError;
|
|
1002
|
+
while (attempt <= maxRetries) {
|
|
1003
|
+
try {
|
|
1004
|
+
const response = await fetch(url, options);
|
|
1005
|
+
if (response.ok) return response;
|
|
1006
|
+
throw await buildResponseError(response);
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
lastError = error;
|
|
1009
|
+
if (!shouldRetryError(error, attempt, maxRetries)) throw error;
|
|
1010
|
+
await sleep(getRetryDelayMs(attempt));
|
|
1011
|
+
attempt += 1;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
throw lastError ?? new Error("LLM API error: request failed after retries");
|
|
1015
|
+
}
|
|
1016
|
+
async function buildResponseError(response) {
|
|
1017
|
+
const errText = await response.text();
|
|
1018
|
+
const error = new Error(`LLM API error: ${response.status} - ${errText}`);
|
|
1019
|
+
error.status = response.status;
|
|
1020
|
+
return error;
|
|
1021
|
+
}
|
|
1022
|
+
function shouldRetryError(error, attempt, maxRetries) {
|
|
1023
|
+
if (attempt >= maxRetries) return false;
|
|
1024
|
+
const status = getErrorStatus2(error);
|
|
1025
|
+
if (typeof status === "number") {
|
|
1026
|
+
return status >= 500;
|
|
1027
|
+
}
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
function getErrorStatus2(error) {
|
|
1031
|
+
return typeof error?.status === "number" ? error.status : void 0;
|
|
1032
|
+
}
|
|
1033
|
+
function getPayload(input) {
|
|
1034
|
+
if ("payload" in input) {
|
|
1035
|
+
return input.payload;
|
|
1036
|
+
}
|
|
1037
|
+
return input.eventPayload;
|
|
1038
|
+
}
|
|
1039
|
+
function getRepoMetadata(payload) {
|
|
1040
|
+
const repoPayload = payload;
|
|
1041
|
+
return {
|
|
1042
|
+
owner: repoPayload?.repository?.owner?.login ?? EMPTY_STRING,
|
|
1043
|
+
repo: repoPayload?.repository?.name ?? EMPTY_STRING,
|
|
1044
|
+
installationId: repoPayload?.installation?.id
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
function buildHeaders(authToken, options) {
|
|
1048
|
+
const headers = {
|
|
1049
|
+
Authorization: `Bearer ${authToken}`,
|
|
1050
|
+
"Content-Type": "application/json"
|
|
1051
|
+
};
|
|
1052
|
+
if (options.owner) headers["X-GitHub-Owner"] = options.owner;
|
|
1053
|
+
if (options.repo) headers["X-GitHub-Repo"] = options.repo;
|
|
1054
|
+
if (typeof options.installationId === "number" && Number.isFinite(options.installationId)) {
|
|
1055
|
+
headers["X-GitHub-Installation-Id"] = String(options.installationId);
|
|
1056
|
+
}
|
|
1057
|
+
if (options.ubiquityKernelToken) {
|
|
1058
|
+
headers["X-Ubiquity-Kernel-Token"] = options.ubiquityKernelToken;
|
|
1059
|
+
}
|
|
1060
|
+
return headers;
|
|
1061
|
+
}
|
|
1062
|
+
async function* parseSseStream(body) {
|
|
1063
|
+
const reader = body.getReader();
|
|
1064
|
+
const decoder = new TextDecoder();
|
|
1065
|
+
let buffer = EMPTY_STRING;
|
|
1066
|
+
try {
|
|
1067
|
+
while (true) {
|
|
1068
|
+
const { value, done: isDone } = await reader.read();
|
|
1069
|
+
if (isDone) break;
|
|
1070
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1071
|
+
const { events, remainder } = splitSseEvents(buffer);
|
|
1072
|
+
buffer = remainder;
|
|
1073
|
+
for (const event of events) {
|
|
1074
|
+
const data = getEventData(event);
|
|
1075
|
+
if (!data) continue;
|
|
1076
|
+
if (data.trim() === "[DONE]") return;
|
|
1077
|
+
yield parseEventData(data);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
} finally {
|
|
1081
|
+
reader.releaseLock();
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
function splitSseEvents(buffer) {
|
|
1085
|
+
const normalized = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1086
|
+
const parts = normalized.split("\n\n");
|
|
1087
|
+
const remainder = parts.pop() ?? EMPTY_STRING;
|
|
1088
|
+
return { events: parts, remainder };
|
|
1089
|
+
}
|
|
1090
|
+
function getEventData(event) {
|
|
1091
|
+
if (!event.trim()) return null;
|
|
1092
|
+
const dataLines = event.split("\n").filter((line) => line.startsWith("data:"));
|
|
1093
|
+
if (!dataLines.length) return null;
|
|
1094
|
+
const data = dataLines.map((line) => line.startsWith("data: ") ? line.slice(6) : line.slice(5).replace(/^ /, EMPTY_STRING)).join("\n");
|
|
1095
|
+
return data || null;
|
|
1096
|
+
}
|
|
1097
|
+
function parseEventData(data) {
|
|
1098
|
+
try {
|
|
1099
|
+
return JSON.parse(data);
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (data.includes("\n")) {
|
|
1102
|
+
const collapsed = data.replace(/\n/g, EMPTY_STRING);
|
|
1103
|
+
try {
|
|
1104
|
+
return JSON.parse(collapsed);
|
|
1105
|
+
} catch {
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1109
|
+
const preview = data.length > 200 ? `${data.slice(0, 200)}...` : data;
|
|
1110
|
+
throw new Error(`LLM stream parse error: ${message}. Data: ${preview}`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
762
1113
|
export {
|
|
763
1114
|
CommentHandler,
|
|
764
1115
|
callLlm,
|